1 HotSpot 虚拟机内的即时编译器
- 为了提高热点代码的执行效率,在运行时虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(JIT);
- JIT 不是虚拟机必需的,但是其编译性能的好坏、代码优化程度的高低却是衡量一款商用虚拟机优秀与否的最关键的指标之一,它也是虚拟机中最核心且最能体现虚拟机技术水平的部分;
1.1 解释器与编译器
当程序需要迅速启动和执行的时候,解释器可以先发挥作用,省去编译的时间立即执行;在程序运行后,随着时间的推移,编译器把越来越多的代码编译成本地代码提升执行效率
1)运行模式
HotSpot虚拟机中内置了两个即时编译器,分别为Client Compiler和Server Compiler,或简称为C1编译器和C2编译器;虚拟机会根据自身版本与宿主机器的硬件性能自动选择运行模式,也可以使用“-client”或“-server”参数去强制指定运行模式;
2)分层编译
想要编译出优化程度更高的代码,解释器可能还要替编译器收集性能监控信息,为了在程序启动响应速度与运行效率之间达到最佳平衡,HotSpot 虚拟机还会逐渐启动分层编译的策略:第 0 层,程序解释运行;第 1 层,C1 编译;第 2 层,C2 编译;
实施分层编译后,Client Compiler 和 Server Compiler 将会同时工作,许多代码都可能会被多次编译,用 Client Compiler 获取更高的编译速度,用 Server Compiler 来获取更好的编译质量,在解释执行的时候也无须再承担性能收集监控信息的任务;
1.2 编译对象与触发条件
1)热点代码
被 JIT 编译的热点代码有两类:被多次调用的方法、被多次执行的循环体;
- 对于前者编译器会以整个方法作为编译对象,属于标准的 JIT 编译方式;
- 对于后者尽管编译动作是由循环体所触发的,但编译器依然会以整个方法作为编译对象,这种编译方式称之为栈上替换(OSR 编译);
2)热点探测
基于采样的热点探测和基于计数器的热点探测,在 HotSpot 虚拟机中使用的是第二种,通过方法计数器和回边计数器进行热点探测。
方法调用计数器触发的即时编译交互过程如下图所示:
1.3 编译过程
1)Client Compiler
对于 Client Compiler 来说,它是一个简单快速的三段式编译器,主要的关注点在于局部性的优化,而放弃了很多耗时较长的全局优化手段;
- 第一阶段一个平台独立的前端将字节码构造成一个高级中间代码表示(HIR)
- 第二阶段一个平台相关的后端从HIR中产生低级中间代码表示(LIR)
- 最后阶段是在平台相关的后端使用线性扫描算法在 LIR 上分配寄存器,并在 LIR 上做窥孔优化,然后产生机器代码。
其大致过程如下所示:
2)Server Compiler
Server Compiler 是专门面向服务端的典型应用并为服务端的性能配置特别调整过的编译器,也是一个充分优化过的高级编译器,几乎能达到 GNU C++ 编译器使用 -02 参数时的优化强大,它会执行所有经典的优化动作,如无用代码消除、循环展开、循环表达式外提、消除公共子表达式、常量传播、基本块重排序等,还会实现如范围检查消除、空值检查消除等 Java 语言特性密切相关的优化技术;
1.4 查看及分析即时编译结果
- 本节的运行参数有一部分需要 Debug 或 FastDebug 版虚拟机的支持;
- 要知道某个方法是否被编译过,可以使用参数 -XX:+PrintCompilation 要求虚拟机在即时编译时将被编译成本地代码的方法名称打印出来;
- 还可以加上参数 -XX:+PrintInlining 要求虚拟机输出方法内联信息
- 除了查看那些方法被编译之外,还可以进一步查看即时编译器生成的机器码内容,这个需要结合虚拟机提供的反汇编接口来阅读;
2 编译优化技术
2.1 优化技术概览
2.2 公共子表达式消除
如果一个表达式E已经计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么 E 的这次出现就成为了公共子表达式,只需要直接用前面计算过的表达式结果代替 E 就可以了;
2.3 数组边界检查消除
对于虚拟机的执行子系统来说,每次数组元素的读写都带有一次隐含的条件判断,对于拥有大量数组访问的程序代码无疑是一种性能负担;
2.4 方法内联
- 除了消除方法调用的成本外更重要的意义是为其他优化手段建立良好的基础;
- 为了解决虚方法的内联问题,引入了类型继承关系分析(CHA)技术和内联缓存(Inline Cache)来完成方法内联;
2.5 逃逸分析
逃逸分析的基本行为就是分析对象动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用(方法逃逸),甚至还可能被外部线程所访问到(线程逃逸);
如果能证明一个对象不会逃逸到方法或线程之外,则可能为这个变量进行一些高效的优化,比如栈上分配(减轻垃圾收集的压力)、同步消除(读写不会有竞争)、标量替换;