前边讲到了即时编译器将字节码翻译为本地机器码,本文介绍的优化技术是即时编译器在生成代码时采用的代码优化技术
消除方法调用的成本,为其他优化手段建立良好的基础,示例如下,只是举例方法内联的含义,实际优化后肯定不是这样的,还会有其他的优化手段继续优化。
- 方法逃逸: 分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中
- 线程逃逸: 当一个对象可能被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸
如果能证明一个对象不会逃逸到方法或线程之外与,或者逃逸程度比较低(只逃逸出方法而不会逃逸出线程),则可能为这个对象实例采取不同程度的优化,比如:
- 栈上分配(HotSpot还没有做这项优化): Java在堆上分配对象的内存空间是大家都知道的尝试,Java堆中的对象对于各个线程都是共享和可见的,只要持有这个对象的引用,就可以访问到堆中存储的对象数据。垃圾收集系统会回收堆中无用对象,但是整个回收的过程需要耗费大量资源。如果确定一个对象不会逃逸出线程之外,那让这个对象在栈上分配内存将会是一个很不错的主意,对象所占用的内存空间就可以随栈帧出栈而销毁,这样垃圾收集系统的压力就会下降很多
- 标量替换:
- 如果一个数据已经无法再分解成更小的数据来表示了,原始数据类型(int、long等数值类型及reference类型等)都不能再进一步分解了,那么这些数据就可以被称为标量。
- 如果一个数据可以继续分解,那它就被称为聚合量(Aggregate),Java中的对象就是典型的聚合量。
- 如果把一个Java对象拆散,根据程序的访问情况,将其用到的成员变量恢复为原始类型访问,这个过程就称为标量替换。假如逃逸分析能够证明一个对象不会被方法外部访问,并且这个对象可以被拆散,那么程序真正执行的时候可能不会去创建这个对象,而改为直接创建它的若干个被这个方法使用的成员变量来代替。将对象拆分后,除了可以让对象的成员变量在栈上(栈上存储的数据,很大机会被虚拟机分配至物理机器的高速寄存器中存储)分配和读写之外,还可以为后续进一步的优化手段创建条件。
- 同步消除:线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写肯定就不会有竞争,对这个变量实施的同步措施也就可以安全地消除掉
:开启逃逸分析
:开启标量替换
:开启同步消除
从JDK6 Update23开始,访问到编译器中开始才默认开启逃逸分析,但目前逃逸分析技术仍在发展之中,未完全成熟。
公共子表达式: 如果一个表达式E之前已经被计算过了,并且从先前的计算到现在E中所有变量的值都没有发生变化,那么E的这次出现就称为公共子表达式。
?
对于这种表达式,没有必要花时间再对它进行重新计算,只需要直接用前面计算过的表达式结果代替E。
举例:
上面这段代码中的公共子表达式为,Javac编译器不会对这段代码进行任何优化,生成的字节码是完全遵照Java源码的写法直译而成的。
但是当这段代码进入虚拟机的即时编译器后,编译器检测到与是一样的表达式,并且在计算期间b与c的值是不变的,因此这条表达式就可能被视为
有的编译器还可能进行另外一种优化----代数化简,在E本来就有乘法运算的前提下,把表达式变为
我们在访问数组元素的时候肯定遇到过,如果有一个数组foo[],在Java语言中访问数组元素foo[i]的时候徐彤将会自动进行上下界的范围检查,即i必须满足“i>=0 && i<foo.length”的访问条件,否则就会抛出运行时异常。
这样对于开发者的好处就是即时我们没有专门编写防御代码,也能够避免大多数的溢出攻击;对于虚拟机的执行子系统来说,每次数组元素的读写都带有一次隐含的条件判定操作,对于大量数组访问的程序代码,这必定是一种性能负担。
?
但是为了安全这种检查肯定是要做的,但数组边界检查不一定必须在运行期间一次不漏的进行,因此Java将一部分数组边界检查操作放到了编译器,例如:
- 当访问foo[3]时,只要在编译器确定foo.length的值,并判定下标“3”没有越界,执行的时候就无须判定了。
- 更常见的就是在循环体中使用循环变量来访问数组了,编译器通过数据流分析就可以判定变量的取值范围永远在[0,foo.length)之内,那么在循环中就可以把整个数组的上下界检查消除掉,着这样可以节省很多次的条件判定操作
还有其他消除操作,比如自动装箱消除,安全点消除,消除反射等。