写在前面
本文隶属于专栏《100个问题搞定Java虚拟机》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢!
本专栏目录结构和文献引用请见100个问题搞定Java虚拟机
解答
即时编译(JIT)。这是一项用来提升应用程序运行效率的技术。
通常而言,代码会先被 Java 虚拟机解释执行,之后反复执行的热点代码则会被即时编译成为机器码,直接运行在底层硬件之上。
即时编译是由方法调用计数器和循环回边计数器触发的。
在使用分层编译的情况下,触发编译的阈值是根据当前待编译的方法数目动态调整的。
补充
Java 中的分层编译是什么?
C1、C2、Graal 是什么?
HotSpot 虚拟机包含多个即时编译器 C1、C2 和 Graal。
其中,Graal 是一个实验性质的即时编译器,可以通过参数 -XX:+UnlockExperimentalVMOptions -XX:+UseJVMCICompiler 启用,并且替换 C2。
在 Java 7 以前,我们需要根据程序的特性选择对应的即时编译器。
对于执行时间较短的,或者对启动性能有要求的程序,我们采用编译效率较快的 C1,对应参数 -client。
对于执行时间较长的,或者对峰值性能有要求的程序,我们采用生成代码执行效率较快的 C2,对应参数 -server。
Java 7 引入了分层编译(对应参数 -XX:+TieredCompilation)的概念,综合了 C1 的启动性能优势和 C2 的峰值性能优势。
分层编译的五个层次
分层编译将 Java 虚拟机的执行状态分为了五个层次。
为了方便阐述,我用“C1 代码”来指代由 C1 生成的机器码,“C2 代码”来指代由 C2 生成的机器码。
五个层级分别是:
- 解释执行;
- 执行不带 profiling 的 C1 代码;
- 执行仅带方法调用次数以及循环回边执行次数 profiling 的 C1 代码;
- 执行带所有 profiling 的 C1 代码;
- 执行 C2 代码。
通常情况下,C2 代码的执行效率要比 C1 代码的高出 30% 以上。
然而,对于 C1 代码的三种状态,按执行效率从高至低则是 1 层 > 2 层 > 3 层。
其中 1 层的性能比 2 层的稍微高一些,而 2 层的性能又比 3 层高出 30%。
这是因为 profiling 越多,其额外的性能开销越大。
在 5 个层次的执行状态中,1 层和 4 层为终止状态。当一个方法被终止状态编译过后,如果编译后的代码并没有失效,那么 Java 虚拟机是不会再次发出该方法的编译请求的。
profiling是什么
profiling 是指在程序执行过程中,收集能够反映程序执行状态的数据。
这里所收集的数据我们称之为程序的 profile。
JDK 附带的 hprof 就是一种 profiler。
这些 profiler 大多通过注入(instrumentation)或者 JVMTI 事件来实现的。
详情可以参考我的这篇博客——一篇文章搞懂 JVM 的 profiling 是什么
profiling 与分层编译的关系
Java 虚拟机内置了 profiling。
如果方法的字节码数目比较少(如 getter/setter),而且 3 层的 profiling 没有可收集的数据。
那么,Java 虚拟机断定该方法对于 C1 代码和 C2 代码的执行效率相同。
在这种情况下,Java 虚拟机会在 3 层编译之后,直接选择用 1 层的 C1 编译。
由于这是一个终止状态,因此 Java 虚拟机不会继续用 4 层的 C2 编译。
在 C1 忙碌的情况下,Java 虚拟机在解释执行过程中对程序进行 profiling,而后直接由 4 层的 C2 编译。
在 C2 忙碌的情况下,方法会被 2 层的 C1 编译,然后再被 3 层的 C1 编译,以减少方法在 3 层的执行时间。
Java8 的分层编译
Java 8 默认开启了分层编译。不管是开启还是关闭分层编译,原本用来选择即时编译器的参数 -client 和 -server 都是无效的。
当关闭分层编译的情况下,Java 虚拟机将直接采用 C2。
如果你希望只是用 C1,那么你可以在打开分层编译的情况下使用参数 -XX:TieredStopAtLevel=1。
在这种情况下,Java 虚拟机会在解释执行之后直接由 1 层的 C1 进行编译。
OSR
OSR 是一种能够在非方法入口处进行解释执行和编译后代码之间切换的技术。
OSR 编译可以用来解决单次调用方法包含热循环的性能优化问题。