JVM Just-In-Time

即时编译是一项用来提升应用程序运行效率的技术。通常而言,代码会先被 Java 虚拟机解释执行,之后反复执行的热点代码则会被即时编译成为机器码,直接运行在底层硬件之上。

HotSpot 虚拟机包含多个即时编译器 C1、C2 和 Graal。

Graal 是一个实验性质的即时编译器,可以通过参数 -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler 启用,并且替换 C2

在 Java 7 以前,我们需要根据程序的特性选择对应的即时编译器。
对于执行时间较短的,或者对启动性能有要求的程序,我们采用编译效率较快的 C1,对应参数 -client。
对于执行时间较长的,或者对峰值性能有要求的程序,我们采用生成代码执行效率较快的 C2,对应参数 -server。

Java 8 默认开启了分层编译。
不管是开启还是关闭分层编译,原本用来选择即时编译器的参数 -client 和 -server 都是无效的。
当关闭分层编译的情况下,Java 虚拟机将直接采用 C2。
如果你希望只是用 C1,那么你可以在打开分层编译的情况下使用参数 -XX:TieredStopAtLevel=1。在这种情况下,Java 虚拟机会在解释执行之后直接由 1 层的 C1 进行编译。

分层编译将 Java 虚拟机的执行状态分为了五个层次。
为了方便阐述,我用“C1 代码”来指代由 C1 生成的机器码,“C2 代码”来指代由 C2 生成的机器码。
五个层级分别是:
解释执行;
执行不带 profiling 的 C1 代码;
执行仅带方法调用次数以及循环回边执行次数 profiling 的 C1 代码;
执行带所有 profiling 的 C1 代码;
执行 C2 代码。
通常情况下,C2 代码的执行效率要比 C1 代码的高出 30% 以上。

即时编译的触发

Java 虚拟机是根据方法的调用次数以及循环回边的执行次数来触发即时编译的。前面提到,Java 虚拟机在 0 层、2 层和 3 层执行状态时进行 profiling,其中就包含方法的调用次数和循环回边的执行次数。

在不启用分层编译的情况下,当方法的调用次数和循环回边的次数的和,超过由参数 -XX:CompileThreshold 指定的阈值时(使用 C1 时,该值为 1500;使用 C2 时,该值为 10000),便会触发即时编译。

References

[1] 16 | 即时编译(上)
[2] 17 | 即时编译(下)
[3] 深入浅出 JIT 编译器
[4] Java Platform, Standard Edition JRockit to HotSpot Migration Guide
[5] Introduction to Graal