JVM C1,C2编译器以及分层编译

及时编译器

当JVM的初始化完成后,类在调用执行过程中,执行引擎会把字节码转换成机器码,然后在操作系统中才能执行。在字节码转换为机器码的过程中,虚拟机中还存在着一道编译,那就是即时编译JIT。

最初,JVM中的字节码是由解释器(Interpreter)完成编译的,当虚拟机发现某个方法或代码块运行特别频繁时,就会把这些代码认定为热点代码。

为了提高热点代码的执行效率,在运行时,即时编译器(JIT, Just In Time)会把这些代码编译成与本地平台相关的机器码,并进行各层次的优化,然后保存到内存中。

怎么样才会被认为是热点代码呢?JVM中会设置一个阈值,当方法或者代码块的在一定时间内的调用次数超过这个阈值时就会被编译,存入codeCache中。当下次执行时,再遇到这段代码,就会从codeCache中读取机器码,直接执行,以此来提升程序运行的性能。整体的执行过程大致如下图所示:

方法调用计数器用于统计方法调用次数,它的默认阈值是client模式下是1500次,在server模式下是10000次。

即时编译器的目的是避免函数被解释执行,而是将整个函数体编译成机器码指令,每次函数执行时,只执行编译后的机器码即可,这种方式可以大大的提高效率。

1、热点代码及探测方式

当然,是否需要JIT编译器将字节码直接编译成对应平台的机器码,需要根据代码被调用的执行频率而定。需要被JIT编译器编译成机器码的字节码,也称为热点代码,JIT编译器会对热点代码做出深度优化,将其从字节码编译成机器码,并缓存到方法区,提高代码的执行效率。JIT编译的方式发生在方法执行过程中,因此也被称之为_栈上替换_,或简称OSR(On Stack Replacement)编译。通过热点探测的方法,判断一个方法被调用多少次,或循环体执行多少次才可以达到阈值,进行编译。而Hotspot VM热点探测的方式是基于计数器实现的。这种基于技术的热点探测方式又分为两种:1.方法调用计数器 2.回边计数器

关于栈上替换这里笔者不展开赘述,有兴趣的小伙伴可以自行了解下

1.1方法调用计数器

方法调用计数器用于统计方法调用次数,它的默认阈值是client模式下是1500次,在server模式下是10000次。超过这个阈值,就会触发JIT编译。当然,这个阈值也可以通过修改虚拟机参数-XX:CompileThreshold来手动指定。当一个方法被调用的时候,会优先检查该方法是否被JIT编译过,如果存在,则优先使用编译过的本地代码来执行,如果不存在,则将此方法的调用计数器加一,然后再判断计数器的值是否超过配置的阈值。如果已经超过了,就会向JIT编译器提交一个该方法的编译请求。下面是方法调用计数器执行的流程图:

关于方法调用计数器,如果不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数,而是一个相对执行的频率。当超过一定的时间限度,如果方法的调用次数仍然达不到阈值,那这个方法的调用计数器就会被减少一半,这个过程称为方法调用计数器的热度衰减,而这段时间被称作为该方法的半衰周期。进行热度衰减的过程是虚拟机进行垃圾回收的时候顺便进行的,举手之劳而已。可以使用虚拟机参数-XX:-UseCounterDecay来关闭热度衰减。这样的话,只要运行时间足够长,绝大部分方法都会被编译成本地代码。最后,还可以使用-XX:CounterHalfLifeTime参数设置半衰周期的时间,单位为秒。

1.2回边计数器

它的作用是统计一个方法中循环体代码执行次数,在字节码中遇到控制流向后,跳转的指令称为“回边”。显然,建立回边计数器统计的目的是为了触发OSR编译。下面是回边计数器执行的流程图:

关于OSR编译上文中有提到

即时编译器分类

在Hotspot VM中,内嵌有两个JIT编译器,分别为client compiler和server compiler,但是大多数情况下我们简称C1编译器和C2编译器。可以通过命令显示的指定JVM在运行时到底使用哪种JIT编译器。

C1编译器

client comiler

指定java虚拟机运行在client模式下,使用C1编译器。C1编译器会对字节码进行简单和可靠的优化,耗时短,已达到更快的编译速度,但是编译后的代码执行速度相对慢。C1编译器主要有方法内联去虚拟化,冗余消除

  1. 方法内联:将引用的函数代码编译到引用点处,这样可以减少栈帧的生成,减少参数传递以及跳转过程。
  2. 去虚拟化:对唯一实现的类进行内联。
  3. 冗余消除:在运行期间把一些不会执行的代码叠掉。

C2编译器

指定Java虚拟机运行在server模式下,使用C2编译器。C2编译器对代码优化时间长,编译时间也长。但是编译后的代码执行速度比较快。C2的优化主要在全局层面,逃逸分析是优化的基础。基于逃逸分析,C2上有如下几种优化:

  1. 标量替换:用标量值代替聚合对象的属性值。
  2. 栈上分配:对于未逃逸的对象分配在栈上而不是堆上。
  3. 同步消除:清除同步操作,通常指synchronized。

Graal编译器

JDK10起,在C1编译器和C2编译器之后,HotSpot VM新增了一个Graal即时编译器。编译效果短短几年的时间就追平了C2编译器。目前,带着“实验状态”标签,需要使用开关参数-XX:+UnlockExperimentalVMOptions-XX:+UseJVMCICompiler去激活这个编译器,才能使用。

解释器和JIT并存

为什么需要解释器和JIT并存,原因有几点:

  1. 当程序启动的时候,解释器可以马上发挥作用,省去编译的时间。
  2. 编译器想要执行,需要把字节码编译成本地机器码,并且缓存编译后的机器码,编译需要一定的时间。
  3. 编译后的本地机器码,执行效率高。所以,在两种并存的模式下,解释器首先发挥作用,而不必等到即时编译器全部编译完再执行,这样可以省去不必要的编译时间。
  4. 随着程序继续不断运行,编译器发挥作用,根据热点探测功能,把越来越多的字节码编译成本地机器码,获得更高的执行效率。

执行引擎执行程序的方式

在默认的情况下,HotSpot VM采用的是解释器和JIT编译器并存的架构,当然读者可以根据具体的应用场景,通过虚拟机参数,为虚拟机指定在运行时到底是完全采用解释器执行,还是完全采用即时编译器执行。

  1. -Xint:完全采用解释器模式执行程序
  2. -XComp:完全采用即时编译器模式执行程序。如果即时编译器出现问题,解释器会介入执行;
  3. -Xmixed:采用解释器+即时编译器的混合模式共同执行程序,HotStop VM默认就是这个模式。

七、参考源码

编程文档:
https://gitee.com/cicadasmile/butte-java-note

应用仓库:
https://gitee.com/cicadasmile/butte-flyer-parent
Last modification:November 13, 2023
如果觉得我的文章对你有用,请随意赞赏