JVM基础知识
编程语言
- 分类
- 面向过程、面向对象、面向函数
- 静态类型、动态类型
- 编译执行、解释执行
- 有虚拟机、无虚拟机
- 有GC、无GC(GarbageCollection)
- 发展过程
- 机器语言
- 编程语言
- 高级语言
- Java是面向对象、静态类型、编译执行、有VM、有GC的运行时跨平台的高级语言
- 跨平台
- 源码跨平台

- 二进制跨平台

Java、C++、Rust区别
Java bytecode是由单字节的指令组成
- 理论上支持256个操作码opcode
- 实际只用了200多个opcode
- 根据指令的性质,主要分4大类
public class HelloBytecode{ public static void main(String[] args){ HelloBytecode obj = new HelloBytecode(); } }
```shell
# 编译
javac demo/jvm0104/HelloBytecode.java
# 查看字节码
javap -c -verbose demo.jvm0104.HelloBytecode
字节码运行时结构
- JVM是一台基于栈的计算机器
- 每个线程都有属于自己的线程栈JVM Stack,用于存储栈帧 Frame
- 每一次方法调用JVM都会自动产生一个栈帧
- 栈帧组成
- 操作数栈
- 局部变量数组
- Class引用,指向常量池中对应的Class

- 栈计算
助记符到二进制
- 字节码单字节指令对应的语义化的表示为助记符
数值处理与本地变量表
- 红框中的store/load _后面的1、2、3对应本地变量表中的槽位slot中的1、2、3
- 1、2、3都是简写指令,所以直接用_连接,超过3后分开写,如24行的 dstore 4
- istore/iload 对应 int类型的存储到本地变量表和加载到栈
- astore/aload 对应 对象类型的存储到本地变量表和加载到栈
- iconst_1 / iconst_2 表示常量 1 和 2 ,对应代码中的1 和 2
- dstore 4 表示把 double 类型的变量写到本地变量表中4号 slot 中,对应的变量名为avg 类型是 double
- i2d 表示 int转换称 double
算术操作与类型转换指令
- Java本身有比较多的类型但在字节码中只有5个类型,大都被int表示
- int
- float
- long
- double
- a 表示对象引用
- 一个int 32位 4个byte
- 一个float 32位 4个byte
- 一个long 64位 8个byte
- 一个double 64位 8个byte
程序流程控制指令
- if_icmpge 43 判断 int 对比 greater equal ,如果满足则jump到#43,如果不满足则继续向下
- iinc int increase
- goto 18 跳转到#18
方法调用指令
- invokestatic
- 调用某个类的静态方法,速度最快
- invokespecial
- 调用构造方法,也可以调用同一个类的private方法,以及可见的超类方法
- invokevirtual
- 如果是具体类型的目标对象,用于调用public、protected和package级的private方法
- virtual虚方法,Java中都是虚方法,子类重写了父类的方法后都无法再调用到父类的方法
- invokeinterface
- 通过接口引用来调用方法时会编译成此指令
invokedynamic
stack 表示运行时需要的栈深度
- locals 表示局部变量表中的槽位数
- args_size 表示参数个数
- 程序计数器PC表示运行时代码执行到第几行(偏移量)
JVM类加载器
类的生命周期
- 加载 Loading :找到class文件
- 验证 Verification:验证格式、依赖
- 准备 Preparation:静态字段、方法表
- 解析 Resolution:符号解析为引用
- 初始化 Initialization:构造器、静态变量赋值、静态代码块
- 使用 Using
- 卸载 Unloading
类的加载时机
- 当虚拟机启动时,初始化用户指定的主类,就是启动执行的main方法所在的类;
- 当遇到用以新建目标类实例的new 指令时,初始化new指令的目标类,就是new一个类的时候要初始化;
- 当遇到调用静态方法的指令时,初始化该静态方法所在的类;
- 当遇到访问静态字段的指令时,初始化该静态字段所在的类;
- 子类的初始化会触发父类的初始化;
- 如果一个接口定义了default方法,那么直接实现或间接实现该接口的类的初始化,会触发该接口的初始化;
- 使用反射API对某个类进行反射调用时,初始化这个类,跟前面一样,反射调用要么是已经有实例了,要么是静态方法,都需要初始化;
当初次调用MethodHandle实例时,初始化该MethodHandle指向的方法所在的类;
不会初始化(可能会加载)
通过子类引用父类的静态字段,只会触发父类的初始化,而不会触发子类的初始化;
- 定义对象数组,不会触发该类的初始化,数组中只是存在对象指针;
- 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类;
- 通过类名获取Class对象,不会触发类的初始化,Hello.class不会让Hello类初始化;
- 通过Class.forName加载指定类时,如果指定参数initialize为false时,不会初始化;
- 通过ClassLoader默认的loadClass方法,不会触发初始化动作,只加载不初始化;
三类加载器
- 启动类加载器 BootstrapClassLoader
- 加载JVM依赖的核心系统类
- java.lang / java.util
- 扩展类加载器 ExtClassLoader
- JDK配置的扩展类路径里的jar或class
- 应用类加载器 AppClassLoader
- 应用的jar或class

自定义加载器
- 自定义加载器可以对同一个类产生不同的作用
- 实现同一个类的模块化、版本化管理
- 扩展加载器和应用加载器在Java9之前都是通过URLClassLoader加载的,URLClassLoader是通过ClassLoader加载的

- 可以显示当前ClassLoader都加载了哪些Jar
- 文件夹内所有的class文件和资源文件用zip方式压缩到一起就是jar包

- 自定义classLoader
-
加载器特点
双亲委托(向上委托)
- 负责依赖
-
添加引用类的几种方式
放到JDK的lib/ext目录下,或者 -Djava.ext.dirs
- java -cp/classpath 或者class文件放到当前路径
- 自定义classLoader加载
- 拿到当前执行类的ClassLoader,反射调用addUrl方法添加Jar或路径(JDK9之后无效,但有更简单的Class.forName(className,initFlag,classLoader)方法)
JVM内存模型
内存堆栈结构
- 每个线程都只能访问自己都线程栈
- 所有原生类型的局部变量都存储在线程栈中,因此对其他线程是不可见的
- 线程可以将一个原生类型的变量值的副本传给另一个线程,但不能共享原生类型的局部变量
- 堆内存中包含了Java代码中创建的所有对象(包括包装类型Byte/Long等),不管是哪个线程创建的
- 不管是创建一个对象并将其赋值给局部变量,还是赋值给另一个对象的成员变量,创建的对象都保存在堆内存中

- 如果是原生类型的局部变量,那么他的内容就全部保存在线程栈上
- 如果是对象引用,栈中局部变量槽位中保存的对象的引用地址,对象的实际内容保存在堆上
- 对象的成员变量与对象本身一起存储在堆上,不管成员变量的类型是原生类型还是引用类型
- 类的静态变量则和类定义一样都保存在堆中

总结
每启动一个线程,JVM就会在栈空间分配对应的线程栈,比如1MB的线程栈空间(-Xss1m)
- JVM常见的三个参数
- Xmx 最大堆内存(第二个Heap的内存通常设置总内存70%,栈/非堆/堆外/JVM自身/系统占30%)
- Xms 最小堆内存(第二个Heap的内存,JVM启动起来就分配的内存,通常和Xmx一致)
- Xss 栈空间大小
- JVM常见的三个参数
- 线程栈也叫方法栈,如果使用了JNI方法,则会分配一个单独的本地方法栈(NativeStack)
- 线程执行过程中,一般有多个方法组成调用栈StackTrace,每执行到一个方法,就会创建对应的栈帧Frame
栈内存结构
- 栈帧是一个逻辑上的概念,具体的大小在一个方法编写完成后基本能确定
- 返回值需要空间存放
- 局部变量需要空间
- 操作数栈
- class指针,标识这个栈帧对应的是哪个类的方法,指向非堆的Class对象
堆内存结构
- 堆内存是所有线程共用的内存空间
- JVM将Heap内存分为两部分
- 年轻代 Young generation
- 伊甸区新生儿Eden space
- 存活区Survivor space(S0S1总有一个是空,YoungGC时就是清空一个,把留存的整理到另一个)
- S0
- S1
- 老年代 Old generation也叫Tenured
- 年轻代里的对象被几次GC后仍然存活的转移到老年代
- 超大对象的分配直接分配到老年代
- 年轻代 Young generation
- Non-Heap本质上还是Heap,只是一般不归GC管理,里面划分了3个内存池
- Metaspace
- 之前叫持久代Permanent generation
- java8后换了名字 Metaspace
- 对象的结构,常量池
- CSS CompressedClassSpace,存放class信息的,和Metasapce有交叉
- CodeCache,存放JIT编译后的本地机器码
- Metaspace
CPU与内存行为
- CPU乱序执行(多核并发乱序执行片段)
- volatile关键字
- 原子性操作
- 内存屏障
总结
- JMM JavaMemoryModel
- JMM规范明确定义了不同的线程之间,通过哪些方式,在什么时候可以看见其他线程保存到共享变量中的值,以及如何在必要的时候对共享变量的访问进行同步,屏蔽各种硬件和操作系统之间的内存访问差异,实现Java并发程序真正的跨平台
- JMM规范的是线程间的交互操作,不管线程内局部变量的操作
- 所有的对象、对象的成员、static变量,数组都必须保存在堆内存中
- 局部变量、方法形参、入参、异常处理语句的入参不允许在线程间共享,不受内存模型的影响
- 多个线程同时对一个变量访问时,只要有一个线程执行的是写操作,就会发生“冲突”
可以被其他线程影响或感知的操作,称为线程间的交互行为,主要有
VM Options
- java命令启动时传入的选项options
- -Dfile.encoding=UTF-8 -Da=1
Program Arguments
标准参数
- 所有JVM都要实现,并且向后兼容
- -D设置系统属性
- 非标准参数
- 默认JVM实现这些参数的功能,但不保证所有的JVM都完全实现,也不保证向后兼容
- -X开头的基本都是传给JVM的
- java -X查看当前JVM支持的非标准参数
非稳定参数
系统属性参数
- -D
- 命令行:A=100 java …
- System.getProperty(“A”)/System.setProperty(“A”,”100”)
- 设置环境变量
- 运行模式参数
- -server
- 启动速度慢
- 运行时性能和内存管理效率很高
- 适用于生产环境
- 64位机器上默认此模式
- -client
- 启动速度快
- 运行时性能和内存管理效率不高
- 适用于调试环境
- -Xint
- 解释模式运行
- JVM解释执行所有的字节码
- 降低运行速度
- -Xcomp
- 编译模式执行
- JVM第一次使用时会把所有的字节码编译成本地代码
- 提高运行效率,但要注意预热
- -Xmixed
- 解释模式和编译模式混合
- JVM默认模式,也是推荐模式
- java -version可以看到此模式信息
- -server
- 堆内存设置参数
- -Xmx
- 指定最大堆内存,比如-Xmx4g
- 限制了Heap部分最大值为4g,这个不包括栈内存和堆外内存
- -Xms
- 指定初始堆内存大小,比如-Xms4g
- 并不是操作系统立刻分配初始值,而是GC先规划好,用到时才分配
- 服务器上需要-Xms和-Xmx一致,否则应用刚启动可能就有很多FullGC,堆内存扩容时也会性能抖动
- -Xss
- 设置每个线程栈的大小
- 如-Xss1m就是指定线程栈为1MB
- 与-XX:ThreadStackSize=1m等价
- -Xmn
- 等价于-XX:NewSize
- 使用G1垃圾回收器不应该设置此选项
- 官方建议设置为-Xmx的1/2-1/4
- -XX:MaxPermSize=size
- JDK1.7之前使用的,对应PermanentGeneration大小
- Java8默认允许的Meta空间无限大,此参数无效
- -XX:MaxMetaspaceSize=size
- Java8默认不限制Meta空间,一般不允许设置此选项
- -XX:MaxDirectMemorySize=size
- 系统可以使用的最大堆外内存
- 这个参数和-Dsun.nio.MaxDirectMemorySize效果相同
- -Xmx
- GC设置参数
- -XX:+UseSerialGC 使用串行垃圾回收器
- -XX:+UseParallelGC 使用并行垃圾回收器
- -XX:+UseG1GC 使用G1垃圾回收器
- -XX:+UseConcMarkSweepGC 使用CMS垃圾回收器
- -XX:+UnlockExperimentalVMOptions -XX:+UseZGC // Java11
- -XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC // Java12+
- 分析诊断参数
- -XX:+HeapDumpOnOutOfMemoryError OOM时自动Dump堆内存
- -XX:HeapDumpPath=PATH 与上面的开关配合使用,指定Dump路径,默认为java启动目录
- -XX:OnError
- 发生致命错误FatalError时执行的脚本,例如执行一些命令或curl一个请求
- java -XX:OnError=”gdb -%p” MyApp # %p表示PID
- -XX:OnOutOfMemoryError 发生OOM时执行的脚本
- -XX:ErrorFile=filename 指定致命错误的日志文件名
- JavaAgent参数
- Agent是JVM中一项黑科技,可以通过无侵入方式做很多事情
- 注入AOP代码
- 执行统计
- -agentlib:libname[=options] 启用native方式的agent,参考LD_LIBRARY_PATH路径
- -agentpath:pathname[=options] 启用native方式的agent
- -javaagent:jarpath[=options] 启用外部的agent库
- -Xnoagent 禁用所有agent
- JAVA_OPTS=”-agentlib:hprof=cpu=samples,file=cpu.samples.log” 开启CPU使用时间抽样分析
- Agent是JVM中一项黑科技,可以通过无侵入方式做很多事情


