Java编译器优化技术
Java编译器优化技术
一、优化技术概览
以下截图为即使编译器优化技术一览:
即时编译器对这些代码优化变换是建立在代码的中间表示或者是机器码之上的,绝不是直接在Java源码上去做的。
二、方法内联
内联可以说是优化之母,因为除了消除方法调用的成本之外,它更重要的意义是为其他优化手段建立了良好的基础,没有内联,多数其他优化都无法有效的进行。
方法内联的优化行为就是把目标方法的代码原封不动的“复制”到发起调用的方法之中,避免发生真实的方法调用。
在Java语言中,只有使用invokespecial指令调用的私有方法、实例构造器、父类方法和使用invokestatic指令调用的静态方法才会在编译器进行解析。其他的Java方法调用都必须在运行时进行方法接收者的多态选择,他们都有可能存在多于一个版本的方法接收者。Java对象的方法默认就是虚方法,Java选择在虚拟机中解决这个问题。
为了解决虚方法的内联问题,Java虚拟机首先引入了一种名为类型继承关系分析(CHA)的技术,这是整个应用程序范围内的类型分析技术,用于确定在目前已加载的类中,某个接口是否有多于一种的实现,某个类是否存在子类、某个子类是否覆盖了父类的某个虚方法等信息。
这样编译器在进行内联时就会分不同的情况采取不同处理:
- 如果是非虚方法,那么直接进行内联就可以了,这种的内联是有百分百的保障的;
- 如果遇到虚方法,则会向CHA查询此方法在当前程序状态下是否真的有多个目标版本可选择,如果只查询到一个版本,那就可以假设应用程序的全貌就是当前的样子进行内联,这种内联被称为守护内联。假如虚拟机加载了导致继承关系发生变化的新类,那么就必须抛弃已经编译好的代码,退回到解释状态进行执行,或者重新进行编译。
- 假如向CHA查询出来的结果是存在多个版本的目标方法可供选择,那即时编译器将使用内联缓存的方式来缩减方法调用的开销。这种状态下方法调用是真正发生的,但是比起直接查虚方法表还是要快一些。
内联缓存是一个建立在目标方法正常入口之前的缓存,它的工作原理大致为:
在未发生方法调用前,内联缓存状态为空,当第一次调用发生后,缓存记录下方法接收者的版本信息,并且每次进行方法调用时都比较接收者的版本。如果以后进来的每次调用的方法接收者版本都是一样的,那么这时它就是一种单态内联缓存。通过该缓存来调用,比用不内联的非虚方法调用,仅多了一次类型判断的开销。但如果出现方法接收者不一致的情况,就说明程序用到了虚方法的多态特性,这时候会退化成超多态内联缓存,其开销相当于真正查询虚方法表来进行方法分派。
所以说在多数情况下Java虚拟机进行的方法内联都是一种激进优化。
三、逃逸分析
逃逸分析与类型继承关系分析一样,并不是直接优化代码的手段,而是为了其他优化措施提供依据的分析技术。
逃逸分析的原理是:
分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种称为方法逃逸;甚至可能被外部线程方法访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸;从不逃逸、方法逃逸到线程逃逸,称为对象由低到高的不同逃逸程度。
针对不同的逃逸可能为这个对象实例采取不同程度的优化,如:
- 栈上分配:如果确定一个对象不会逃逸到线程之外,那么让这个对象在栈上分配内存,对象所占用的内存空间就会随着栈帧出栈而销毁。如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁了,垃圾收集子系统的压力将会下降很多。栈上分配支持方法逃逸,但不能支持线程逃逸。
- 标量替换:若一个数据无法再分解成更小的数据来表示了,Java虚拟机中的原始数据类型都不能再进行分解了,那么这些数据就可以被称为标量。相对的,如果一个数据可以继续分解,那么这些数据就被称为聚合量。如果把一个Java对象拆散,根据程序访问的情况,将其使用到的成员变量恢复为原始类型来访问,这个过程就称为标量替换。假如逃逸分析能够证明一个对象不会被方法外部访问,并且可以被拆散,那么程序真正执行的时候将可能不去创建这个对象,而是改为直接创建它的若干个被这个方法使用的成员变量来代替。
- 同步消除:线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,那么这个变量的读写肯定不会有竞争,对这个变量实施的同步措施也就可以安全的消除掉。
关于逃逸分析,在实际的应用程序中,尤其是大型程序中可能出现效果不稳定的情况,或分析过程耗时但却无法有效判别出非逃逸对象而导致性能下降。
四、公共子表达式消除
如果一个表达式E之前已经被计算过,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就被称为公共表达式。对于这种表达式,没有必要花时间再对它重新进行计算,只需要直接用前面计算过的表达式结果代替E。如果这种优化仅限于程序基本块内,便可称为局部公共子表达式消除,如果这种优化涵盖了多个基本块,那就是全局公共子表达式消除。
五、数组边界检查消除
数组边界检查消除是即时编译器中的一项语言相关的优化技术。
Java语言是一门动态安全的语言,如果有一个数组,在Java语言中访问数组元素[i]的时候,系统会自动进行上下界的范围检查,即i必须满足i>=0&&i<length的访问条件,否则抛出一个运行时异常。这对于虚拟机的执行子系统而言,每次数组元素的读写都带有一次隐含条件判定操作,对于拥有大量数组访问的程序代码,这必定是种性能负担。
例如:数组下标是一个常量,foo[3],只要在编译器根据数据流分析确定数组长度的值,并且判断下标3没有越界,执行的时候就无需判断了。
更加常见的情况是数组访问在循环中,并且使用循环变量来访问数组元素,如果编译器可以通过数据流分析判断出循环变量的取值范围在数组的安全范围内,就可以取消条件判断。
除了如数组边界检查优化这种尽可能把运行期检查提前到编译器完成的思路外,还有一种思路就是隐式异常处理。Java中空指针异常和算术运算中除数为0的检查都采用了这种方案。
虚拟机会注册一个Segment Fault信号的异常处理器,这样如果程序没有任何问题,则无需多余的操作,而代价就是当出现异常操作,则必须转到异常处理器中恢复中断。进入异常处理器的过程涉及从用户态到内核态,再从内核态到用户态的转换。