深入理解JVM
JVM总览
- JVM分为三大类,类加载子系统,运行时数据区和执行引擎
- 其中类加载子系统是用来加载编译过的class文件,然后把class元数据信息放入运行时数据区,再有执行引擎执行
- 本文基于HotSpot虚拟机,JDK1.8
运行时数据区
堆
- 堆是线程共享的,但是每个线程也有一小块私有区域叫TLAB,线程创建对象会先放在这个区域,避免分配内存冲突
- 大部分的对象都会分配在堆中,堆是虚拟机中最大的一块区域
- 堆中又划分为新生代和老年代
- 新生代又分为Eden区和两个Survivor区,分别是From区和To区
虚拟机栈
- 虚拟机栈是线程私有的,跟着线程一起创建和销毁
- 栈是先进后出的
- 虚拟机栈中会放置栈帧,而栈帧就是一个个的方法,每次调用方法就会放入一个栈帧,方法执行完毕就会弹出一个栈帧
- 栈帧中包含局部变量表,操作数栈,动态链接和返回地址等
- 局部变量表中包含了当前方法使用的所有变量
- 操作数栈就是用来计算信息,比如变量a加b,就是a出栈b出栈然后计算出c在入栈
- 动态链接就是指向运行时常量池,可以让方法把符号引用改成直接引用,比如调用了别的方法,在加载时会把这个方法调用加载成符号引用,这时可以用过运行时常量池找到对应的直接引用
本地方法栈
- 本地方法栈也是线程私有的,与虚拟机栈类似
- 本地方法栈是用来调用本地方法的栈,本地方法是C代码,不是JAVA
程序计数器
- 程序计时器也是私有的
- 主要用来记录执行到哪一行,因为CPU切换上下文需要知道执行到了哪里,以便接着执行
元空间
- 元空间是共享的,存在于非堆空间
- 元空间是在JDK1.8以后才引进的概念,以前都叫方法区
- 元空间主要包含了加载的类信息,字符串常量池,运行时常量池等
类加载子系统
- 虚拟机加载类经过加载,链接和初始化等步骤,链接又分为验证,准备和解析
加载:
- 创建某个类的实例或者被引用时这个类如果没有被虚拟机加载,则虚拟机会加载这个类。本地类库会被引导类加载器(Bootstrap ClassLoader)加载,扩展类库由扩展类加载器(ExtClassLoader)加载,一般的类比如我们自己写的类就会由应用程序类加载器(AppClassLoader)加载
- 双亲委派机制:在加载器加载类时,会优先给自己的父类加载,直到引导类加载器,如果父类不加载,则才会自己加载。这种机制是为了保证类库的正常使用,比如说我们也自己定义了一个java.lang.String,如果是应用程序加载器加载就会出现问题
- 破坏双亲委派机制:实现ClassLoader接口,重写loadClass方法,不调用super.loadClass()
验证:
- JVM验证编译的代码是否语法没有错误,是否会危害虚拟机等
准备:
- 开始为静态变量分配内存,设初始值。如果是常量则会直接赋值
- 会为非私有的实例方法创建一个数组类型的方法表,这样就可以实现动态方法。也就是在通过父类new出子类并调用方法时,通过动态的类型来调用方法表,获取对应的实例方法并调用
解析:
- 将编译期的符号引用通过运行时常量池找到对应的地址,然后修改为直接引用
初始化:
- 虚拟机会把所有需要赋值的语句全部放在构造器()中,比如对静态变量赋初值等
执行引擎
解释器
- 解释器是一行一行的代码解释执行,因为每次执行都需要解释执行,所以执行效率不是很高,但是胜在拿来即用,虚拟机刚开始运行时就是先解释执行,然后再由JIT对热点代码编译成机器码执行在硬件上
即时编译器
- 即时编译器分为两种,C1和C2,C1适用于时间短,启动快的场景,C2适用时间长,性能要求比较高的场景。根据场景也叫Client Compiler 和 Server Compiler
- 在JDK1.7时引入了分层编译,1.8中默认开启,其中分为5个层次
- 解释执行
- C1编译,但是不开启Profiling(性能检测功能)
- C1编译,仅开启Profiling的方法调用次数和循环回边执行次数
- C1编译,开始所有Profiling
- C2编译
- 方法调用计数器:统计方法的调用次数,C1是1500次,C2是10000次,次数达到时就会触发JIT,分层情况下虚拟机会动态调整
- 循环回边计数器:统计循环代码体执行次数,分层情况也会动态调整
编译优化
- 在编译器编译时,会对代码进行优化,比如方法内联,逃逸分析等
- 方法内联:对于方法体不是太大的方法,虚拟机可以直接把被调用方法转换成当前方法中的代码,这样可以避免被调用方法的入栈出栈等操作
- 逃逸分析:对于一个方法中的对象,我们可以判断这个对象是否会逃逸,也就是会不会作用到别的作用域。如果在当前方法创建了一个对象,这个对象并没有传给别的方法等,只是作用在这个方法,那么我们就称这个对象没有逃逸
- 基于逃逸分析的优化有锁消除,标量替换等
- 锁消除:对不会进行线程竞争的锁会被优化掉。比如synchronized(new String),这样就一定不会线程竞争
- 标量替换:把一个对象替换成对应的局部变量,这样就可以避免在堆里创建对象。比如一个Student类中只有一个int age,一个String name这两个变量,那么就不创建这个对象,在局部变量表中添加这两个字段,然后跟栈一起被回收