HotSpot即时编译器优化
关于即时编译器
当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”。
为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler),简称JIT编译器。
热点探测方法
“热点代码”有两类:
- 被多次调用的方法
- 被多次执行的循环体
由方法调用触发的编译,会把整个方法作为编译对象。
由循环体触发编译,依然会以整个方法作为编译对象,这种编译方式因为编译发生在执行过程中,因此形象地称之为栈上替换(On Stack Replacement,简称OSR编译)。
基于采样的热点探测
虚拟机会周期地检查各个线程的栈顶,如果发现某个方法经常出现在栈顶,那这个方法就是“热点方法”。
基于计数器的热点探测
虚拟机会为每个方法建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值就认定它是“热点方法”。
为每个方法准备了两类计数器:方法调用计数器和回边计数器。
方法调用计数器
具体流程如下图所示:
回边计数器
统计一个方法中循环体代码执行的次数,在字节码中遇到控制流向后跳转的指令称为“回边”。
具体流程如下图所示:
编译优化技术
公共子表达式消除
如果一个表达式E已经计算过了,并且从先前的计算到现在E中所有的变量都没有发生变化,那么E的这次出现就成为了公共子表达式。
对于这种表达式,没有必要花时间再对它进行计算,只需要直接用前面计算过的表达式结果代替E就可以了。
数组边界检查消除
Java语言访问数组元素的时候,系统会自动进行上下界的范围检查,也就是说每次数组元素的读写都带有一次隐含的条件判定操作。
一个常见的情况就是数组访问放生在循环之中,并且用循环变量来进行数组访问,如果编译器只要通过数据流分析就可以判定循环变量的取值范围永远在区间[0, length)之内,那 在整个循环中就可以把数组的上下界检查消除。
方法内联
不仅仅只是把目标方法的代码复制到发起调用的方法之中,因为Java的许多方法都是在运行时才知道真正调用的方法(重写)
解决方案:
- 首先引入“类型继承关系分析”(Class Hierarchy Analysis,CHA),基于整个应用程序的类型分析技术,用于确定在目前已加载的类中,某个接口是否有多于一种的实现,某个类是否存在子类、子类是否为抽象类等信息。
- 首先向CHA查询此方法在当前程序下是否有多个目标版本可供选择,如果只有一个版本,那可以内联。如果加载了导致继承关系发生变化的新类,那就需要抛弃已经编译的代码,退回到解释状态执行,或重新编译
- 如果想CHA查出有多个版本可供选择,那么使用内联缓存完成方法内联。工作原理大致是:未发生方法调用之前,内联缓存是空的,当第一次调用发生后,缓存记录下方法接收者的版本信息,并且每次进行方法调用时都比较接受者版本,如果一致,可以一直用下去。如果不一致,取消内联,进行方法分派。
逃逸分析
当一个对象在方法中被定义后,它可能被外部方法所引用。
例如作为调用参数传递到其他方法中,称为方法逃逸。
甚至可能被外部线程访问到,比如赋值为类变量或可以在其他线程中访问的实例变量,称为线程逃逸。
- 栈上分配:如果一个对象不会逃逸出方法之外,那么这个对象可以在栈上分配内存。
- 同步消除:如果一个变量不会逃逸出线程,那对这个变量实施的同步措施也可以消除掉。
- 标量替换:把一个Java对象拆散,将其使用到的成员变量恢复原始类型来访问。
参考
- 深入理解Java虚拟机[书籍]