即时编译器 JIT
目前JVM一般都是解释器和编译器并存的架构。
解释器负责直接启动和运行,编译器负责优化,编译为本地代码。
当编译器激进优化的时候,出现不正确的情况时,可以采用解释器逆向优化。
目前 Hotspot 内置了几个即时编译器,如 C1 - 客户端编译器 , C2 - 服务器编译器 (JDK10之后出现了 C2 Graal 编译器)
客户端编译器将字节码编译为本地代码,是一个比较简单可靠的稳定优化,即后文中的方法和回文检测功能,不开性能监控优化的功能。
服务器端编译器一般开启了性能监控优化,采取一些丧失稳定性的激进优化。
JDK7开始,就不在采取单一编译器模式,而是采取分层编译的模式
解释器、C1 C2 同时工作,客户端编译器获取更高的编译速度,服务端编译器获取更高的编译质量 ;
客户端编译器可以先为服务器编译器提前做一些简单的优化。
对于客户端编译器,如何判断哪些对象什么时候开始编译?
客户端编译器观察的目标主要是: 多次调用的方法 和 多次执行的循环体
多次调用的方法:编译目标是整个方法体
多次执行的循环体:替换方法第几条字节码指令 - 栈上优化
那么如何感知调用次数?
- 基于采样的热点探测:周期性检查线程的调用栈顶
- 基于计数器的热点探测:
- 方法调用计数器 : 调用即增加,周期性衰减一半
- 回边计数器:字节码控制流回跳就增加,结束循环置零
编译的优化过程
当编译器开始优化的时候,编译动作一般都是后台的编译线程执行的。主要分为三个阶段:
- 字节码转为高级中间代码 HIR : 与目标机器指令集无关的中间标识,完成方法内联、常量传播
- HIR转为低级中间代码LIR:与目标机器指令集有关的中间标识,如空值检查、范围检查
- LIR转为机器代码:基于LIR做窥孔优化、寄存器分配
提前编译器 AOT
提前将Java字节码编译为本地代码 , 意味着丧失了平台中立性、动态扩展特性,一切只为了高性能。
主要分为:
- 类似C编译器一样, 程序运行之前将代码全部转为静态编译工作,耗时的过程间分析放在镜像阶段
- 将JIT做的编译工作提前保存下来,下一次使用的时候直接使用 , 本质就是即时编译缓存
- 提前编译出来的质量是低于JIT运行期间编译的质量:毕竟不知道目标机器的信息、JVM信息(无法生成内存屏障代码)
- JIT可以完成运行期间的分支预测、为热点代码分配寄存器和缓存
- 别忘JIT可以激进预测性优化,如果这个大杀器使用不当,大不了回到C1甚至到解释器上解释执行
这里关联几种常见的编译优化技术: