1. 编译器优化技术
编译器的目标虽然是做程序代码翻译为本地机器 码的工作,但其实难点并不在于能不能成功翻译出机器码,输出代码优化质量的高低才是决定编译器优秀与否的关键。
1.1. 优化技术概览
即时编译器对这些代码优化变换是建立在代码的中间表示或者机器码之上的,绝不是直接在Java源码上去做的。
方法内联->冗余访问消除->复写传播->无用代码消除
-
最重要的优化技术之一:方法内联
-
最前沿的优化技术之一:逃逸分析
-
语言无关的经典优化技术之一:公共因子表达式消除
-
语言相关的经典优化技术之一:数组边界检查消除
1.2. 方法内联
方法内联可以说是最重要的技术,可以去掉“之一”
作用:除了消除方法调用的成本之外,它更重要的意义是为其他优化手段建立良好的基础。
过程:表面上方法内联的优化行为就是把目标方法的代码原封不动的“复制”到发起调用的方法之中,避免发生真实地方法调用而已。
遇到问题:对于一个虚方法,编译器静态地去做内联的时候很难确定应该使用哪个版本方法版本,在Java选择了在虚拟机中解决这个问题。
解决方法:类型继承关系分析(CHA):这是整个应用程序范围内的类型分析技术,用于确定在目前已加载类中,某个接口是否有多于一种的实现、某个类是否存在子类、某个子类是否覆盖了父类的某个虚方法等信息。
解决过程:
如果是非虚方法,那么直接进行内联就可以了,这种内联是有百分百安全保证的。
如果遇到虚方法,则会向CHA查询此方法在当前程序状态下是否真的有多个目标版本可供选择,如果查询到只有一个版本,那就可以假设“应用程序的全貌就是现在运行的这个样子”来进行内联,这种内联被称为守护内联,内置“逃生门”,如果当前条件不成立,就会退回到解释状态进行执行。
内联缓存:如果该方法确实有多个版本的目标方法可供选择,那即时编译器还将进行最后一次努力,使用内联缓存的方式来缩减方法调用的开销。
工作原理:在未发生方法调用之前,内联缓存状态为空,当第一次调用发生后,缓存记录下方法接收者的版本信息,并且每次进行方法调用时都比较接收者的版本。如果以后进来的每次调用的方法接收者版本都是一样的,那么这时他就是一种单态内联缓存。通过该缓存来调用,比用不内联的非虚方法调用,仅多了一次类型判断的开销而已。
通过该缓存来调用,比用不内联的非虛方法调用,仅多了一次类型判断的开销而已。但如果真的出现方法接收者不一致的情况,就说明程序用到了虛方法的多态特性,这时候会退化成超多态内联缓存(Megamorphic Inline Cache),其开销相当于真正查找虚方法表来进行方法分派。
1.3. 逃逸分析
逃逸分析的基本原理是:分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种称为方法逃逸;甚至还有可能被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸;从不逃逸、方法逃逸到线程逃逸,称为对象由低到高的不同逃逸程度。
由逃逸程度从高到底,虚拟机会采取不同的优化:
栈上分配:如果确定一个象不会逃逸出线程之外,那让这个对象在栈上分配内存将会是一个很不错的主意,对象所占用的内存空间就可以随栈帧出栈而销毁。在一般应用中,完全不会逃逸的局部对象和不会逃逸出线程的对象所占的比例是很大的,如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁了,垃圾收集子系统的压力将会下降很多。栈上分配可以支持方法逃逸,但不能支持线程逃逸,(将对象分配到栈上,这个思想很不错)。
标量替换:
-
标量:若一个数据已经无法再分解成更小的数据来表示了,Java虛拟机中的原始数据类型(int、long等数值类型及reference类型等)都不能再进一步分解了,那么这些数据就可以被称为标量。
-
聚合量:如果一个数据可以继续分解,那么它就被称为聚合量。
工作原理:
假如逃逸分析能够证明一个对象不会被方法外部访问,并且这个对象可以被拆散,那么程序真正执行的时候将可能不去创建这个对象,而改为直接创建它的若干个被这个方法使用的成员变量来代替。将对象拆分后,除了可以让对象的成员变量在栈上(栈上存储的数据,很大机会被虛拟机分配至物理机器的高速寄存器中存储)分配和读写之外,还可以为后续进一步的优化手段创建条件。
标量替换可以看作栈上分配的一种特殊案例,实现更简单,但对逃逸程度的要求更高,他不允许对象逃逸出方法范围内。
同步消除:
线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定就不会有竞争,对这个变量实施的同步措施也就可以安全地消除掉。
不成熟的原因:不成熟的原因主要是逃逸分析的计算成本非常高,甚至不能保证逃逸分析带来的性能收益会高于它的消耗,尤其是大型程序中反而发现实施逃逸分析可能出现效果不稳定的情况。如果逃逸分析完毕后发现几乎找不到几个不逃逸的对象,那这些运行器好用的时间就白白浪费了。
1.4. 公共子表达式消除
公共子表达式:如果一个表达式E之前已经被计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就称为公共子表达式。
1.5. 数组边界检查消除
超出边界抛出异常