即时编译器 JIT

目前JVM一般都是解释器和编译器并存的架构。

解释器负责直接启动和运行,编译器负责优化,编译为本地代码。

当编译器激进优化的时候,出现不正确的情况时,可以采用解释器逆向优化。

目前 Hotspot 内置了几个即时编译器,如 C1 - 客户端编译器 , C2 - 服务器编译器 (JDK10之后出现了 C2 Graal 编译器)

客户端编译器将字节码编译为本地代码,是一个比较简单可靠的稳定优化,即后文中的方法和回文检测功能,不开性能监控优化的功能。

服务器端编译器一般开启了性能监控优化,采取一些丧失稳定性的激进优化。

JDK7开始,就不在采取单一编译器模式,而是采取分层编译的模式

解释器、C1 C2 同时工作,客户端编译器获取更高的编译速度,服务端编译器获取更高的编译质量 ;

客户端编译器可以先为服务器编译器提前做一些简单的优化。

对于客户端编译器,如何判断哪些对象什么时候开始编译?

客户端编译器观察的目标主要是: 多次调用的方法 和 多次执行的循环体

多次调用的方法:编译目标是整个方法体

多次执行的循环体:替换方法第几条字节码指令 - 栈上优化

那么如何感知调用次数?

  • 基于采样的热点探测:周期性检查线程的调用栈顶
  • 基于计数器的热点探测:
    • 方法调用计数器 : 调用即增加,周期性衰减一半
    • 回边计数器:字节码控制流回跳就增加,结束循环置零

JIT - 即时编译

编译的优化过程

当编译器开始优化的时候,编译动作一般都是后台的编译线程执行的。主要分为三个阶段:

  • 字节码转为高级中间代码 HIR : 与目标机器指令集无关的中间标识,完成方法内联、常量传播
  • HIR转为低级中间代码LIR:与目标机器指令集有关的中间标识,如空值检查、范围检查
  • LIR转为机器代码:基于LIR做窥孔优化、寄存器分配

提前编译器 AOT

提前将Java字节码编译为本地代码 , 意味着丧失了平台中立性、动态扩展特性,一切只为了高性能。

主要分为:

  • 类似C编译器一样, 程序运行之前将代码全部转为静态编译工作,耗时的过程间分析放在镜像阶段
  • 将JIT做的编译工作提前保存下来,下一次使用的时候直接使用 , 本质就是即时编译缓存
    • 提前编译出来的质量是低于JIT运行期间编译的质量:毕竟不知道目标机器的信息、JVM信息(无法生成内存屏障代码)
    • JIT可以完成运行期间的分支预测、为热点代码分配寄存器和缓存
    • 别忘JIT可以激进预测性优化,如果这个大杀器使用不当,大不了回到C1甚至到解释器上解释执行

这里关联几种常见的编译优化技术:

常见的编译优化技术