JVM学习笔记之执行引擎

目录

背景

概述

java代码编译和执行过程

解释器与JIT编译器

静态提前编译器AOT(Ahead Of Time)

JIT编译器

方法调用计数器和回边计数器

方法调用计数器

回边计数器

设置程序执行方式

server模式和client模式

C1编译器

C2编译器

分层编译


背景

下面是JVM执行引擎的学习笔记,另外执行引擎的执行示例可以参见文章JVM学习笔记上(概述-本地方法栈)中操作数栈相关部分

概述

虚拟机的执行引擎是由软件自行实现的,能够执行那些不被硬件直接支持的指令集格式

JVM学习笔记之执行引擎_第1张图片

JVM主要任务把字节码装载到内部,执行引擎负责把字节码指令解释/编译为对应平台上的机器指令

 

执行引擎在执行过程中执行的字节码指令完全依赖于PC寄存器。每执行完一条指令后,PC寄存器就会更新下一条指令的地址。方法在执行过程中,执行引擎可能会通过存储在局部变量表中的对象引用准确定位到存储到堆空间中的对象实例,以及通过对象头中的元数据指针定位目标对象的类型信息。

JVM学习笔记之执行引擎_第2张图片

 

从外观上看,所有的JVM执行引擎的输入输出都是一致的,输入是字节码的二进制流,处理过程是字节码解析执行的等效过程,输出为执行结果

java代码编译和执行过程

大部分的程序代码转换成物理机的目标代码或虚拟机指令之前,都要经过下图中的各个步骤

JVM学习笔记之执行引擎_第3张图片

对于java语言,从程序源码到生成抽象语法树的部分由前端编译器javac负责;抽象语法树到解释执行是解释器的工作,抽象语法树到目标代码是后端编译器的工作

java代码编译的流程图如下所示

JVM学习笔记之执行引擎_第4张图片

java字节码的执行过程如下图所示

JVM学习笔记之执行引擎_第5张图片

解释器与JIT编译器

解释器:一行一行解释字节码指令,把字节码指令翻译成机器指令执行

JIT编译器:把热点字节码编译成机器语言,而不是立刻执行

 

解释器优势:响应速度快,省去编译时间

JIT编译器优势:执行速度快

静态提前编译器AOT(Ahead Of Time)

直接把java文件编译成本地机器代码,可以提高第一次运行的速度,但必须为每个不同硬件、OS提供对应的发行包,而且降低了java链接过程的动态性,还在继续优化,目前仅支持Linux x64

JIT编译器

根据代码执行频率选择出被反复执行的热点代码,对其进行深度优化,并编译成本地机器指令。

典型的热点代码,比如多次调用的方法、循环多次的循环体,由于这种编译方式发生在栈上,所以称之为栈上替换OSR(On Stack Replacement)

 

热点代码的反复执行阈值的确定,依赖于热点探测功能,目前HotSpot采用的热点探测是基于计数器的热点探测

方法调用计数器和回边计数器

HotSpot会给每一个方法建立两个不同类型的计数器,分别为方法调用计数器和回边计数器

方法调用计数器

方法调用计数器统计方法的调用次数,回边计数器统计循环体循环的次数

 

方法调用计数器的阈值,在client模式下是1500次,在server模式下是10000次。超过这个阈值,就会触发JIT编译

这个阈值可以通过参数-XX:CompileThreshold来设定

当一个方法被调用时,会先检查此方法是否存在被JIT编译过的版本,如果存在,直接执行对应的机器码;如果不是,方法调用计数器+1,判断计数器值是否超过阈值,那么进行JIT编译成机器指令,并缓存到JIT的方法缓存中,最后执行机器码;如果计数器值没有超过阈值,就进行解释执行。

JVM学习笔记之执行引擎_第6张图片

热度衰减与半衰期:

方法调用计数器统计的是一段时间内的方法被调用次数,当超过一定的时间限度,并且这个方法的调用次数还不足以触发JIT编译,那么就会把它的调用次数减半,这一过程就是热度衰减,这段时间长度称为半衰周期。

 

热度衰减的执行是垃圾回收时顺便进行的,可以使用参数-XX:-UseCounterDecay来关闭热度衰减,这时方法调用计数器统计的就是方法的绝对调用次数

半衰期可以使用参数-XX:CounterHalfLifeTime来设置,单位为秒

回边计数器

回边计数器用来统计某个循环体执行次数,字节码中遇到控制流向后跳转的情况称之为回边,此计数器用来触发OSR编译。

对于某个循环体,它的回边计数器值和所在方法的方法调用计数器的和超过阈值时,就会触发JIT编译

 

设置程序执行方式

-Xint:纯解释器执行

-Xcomp:纯JIT执行

-Xmixed:混合执行

C:\Users\songzeceng>java -Xint -version
java version "1.8.0_231"
Java(TM) SE Runtime Environment (build 1.8.0_231-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.231-b11, interpreted mode)


C:\Users\songzeceng>java -Xcomp -version
java version "1.8.0_231"
Java(TM) SE Runtime Environment (build 1.8.0_231-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.231-b11, compiled mode)


C:\Users\songzeceng>java -Xmixed -version
java version "1.8.0_231"
Java(TM) SE Runtime Environment (build 1.8.0_231-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.231-b11, mixed mode)

server模式和client模式

64位默认server模式(而且是只能64位),可以通过-client和-server指定JVM运行模式

server模式使用C2编译器(C++实现),优化时间较长,方式比较激进,代码执行效率高

client模式使用C1编译器,执行简单可靠的优化,耗时短,但代码执行效率低

C1编译器

C1编译器主要进行方法内联、去虚拟化、冗余消除

方法内联:将引用的函数代码编译到引用点处,这样可以减少栈帧的生成,减少参数的传递和跳转过程

去虚拟化:对唯一的实现类进行内联

冗余消除:在运行期间把一些不会执行的代码折叠掉

C2编译器

C2编译器使用的优化是基于逃逸分析的优化,主要有标量替换、栈上分配和同步消除

标量替换:用标量值替换聚合对象的属性值

栈上分配:对于未逃逸的对象分配在栈上,而不是堆里

同步清除:清除同步操作

分层编译

分层编译:如果不开启性能监控,那么程序解释执行可以触发C1编译;开启性能监控,C2编译器会根据性能监控信息进行激进优化

jdk8之后,server模式会开启分层编译策略,C1和C2会协同来执行编译任务

 

C2编译器启动比C1慢,但代码执行效率要高得多

你可能感兴趣的:(JVM)