逃逸分析(Escape Analysis)是目前 Java 虚拟机中比较前沿的优化技术,逃逸分析是一种确定指针动态范围的静态分析,它可以分析在程序的哪些地方可以访问到指针。与类继承关系分析(CHA)一样,逃逸分析并不是直接优化代码的手段,而是为其他优化措施提供依据的分析技术。
逃逸分析的基本原理是:分析对象动态作用域来判断新建的对象是否逃逸。当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种称为 方法逃逸;甚至还有可能被外部线程访问到,譬如赋值给可以在其他线程中访问的静态字段或堆中对象的实例字段,这种称为线程逃逸。
如果能证明一个对象不会逃逸到方法或线程之外,或者逃逸程度比较低(只逃逸出方法而不会逃逸出线程),则即时编译器可能为这个对象实例采取不同程度的优化,诸如栈上分配、标量替换以及同步消除的优化。
官方文档:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/performance-enhancements-7.html#escapeAnalysis
栈上分配(Stack Allocations)
在 Java 虚拟机中,Java 堆中的对象对于各个线程都是共享和可见的,只要持有这个对象的引用,就可以访问到堆中存储的对象数据。虚拟机的垃圾收集子系统会回收堆中不再使用的对象,但回收动作无论是标记筛选出可回收对象,还是回收和整理内存,都需要耗费大量资源。
如果逃逸分析能够确定一个对象不会逃逸出线程外,那让这个对象在栈上分配内存将是一个不错的主意,对象所占用的内存空间就可以随栈帧出栈而被销毁。通常完全不会逃逸的局部对象和不会逃逸出线程的对象所占比例是很大的,如果使用栈上分配,那大量的对象就会随着方法的结束而自动销毁,垃圾收集子系统的压力将会下降很多。栈上分配可以支持 方法逃逸,但不能支持 线程逃逸。
不过,由于栈上分配实现起来需要更改大量假设了“对象只能堆分配”的代码,因此 HotSpot 虚拟机并没有采用栈上分配,即对于 HotSpot 虚拟机来说,不会将对象分配在栈上,而是使用了标量替换这一技术。
标量替换(Scalar Replacement)
若一个数据已经无法再分解成更小的数据来表示,如 Java 虚拟机中的原始数据类型,那它就被称为 标量,即仅能存储一个值的变量。相反,如果一个数据可以继续分解,那它就被称为 聚合量,如 Java 中的对象。标量替换这项优化技术,可以看成将原本对对象的字段的访问,替换为一个个局部变量的访问。
假如逃逸分析能证明一个对象不会被方法外部访问,并且这个对象可以被拆散,那么程序真正执行时可能不会去创建这个对象,而改为直接创建它的若干个被这个方法使用的成员变量来代替。将对象拆分后,除了可以让对象的成员变量在栈上分配和读写外,还可以为后续进一步的优化手段创建条件。标量替换可以视作是栈上分配的一种特例,实现更简单但对逃逸程度的要求更高,它不允许对象逃逸出方法范围内。
标量字段既可以存储在栈上,也可以直接存储在寄存器中。而原对象的对象头信息则直接消失了。由于该对象没有被实际分配,因此和栈上分配一样,它同样可以减轻垃圾回收的压力。与栈上分配相比,它对字段的内存连续性不做要求,而且这些字段甚至可以直接在寄存器中维护,无须浪费任何内存空间。
同步消除(Synchronization Elimination)
线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问到的话,那这个变量的读写肯定就不会有竞争,对这个变量实施的同步措施也就可以安全地消除掉。
实际上,传统编译器仅需证明锁对象不逃逸出线程,便可以进行锁消除。由于 Java 虚拟机即时编译的限制,上述条件被强化为证明锁对象不逃逸出当前编译的方法。
比如对于 synchronized (new Object()) {} 这段同步代码,由于逃逸分析的同步消除,这段同步代码会被完全优化掉。而对于 synchronized (escapedObject) {} 则不然,由于其他线程可能会对逃逸了的对象 escapedObject 进行加锁操作,从而构造了两个线程之间的 happens-before 关系。因此即时编译器至少需要为这段代码生成一条刷新缓存的内存屏障指令。
总结
逃逸分析直到 JDK 7 时才成为服务端编译器默认开启的选项。如果有需要,用户也可以使用参数 -XX:+DoEscapeAnalysis 来手动开启逃逸分析,开启后可以通过参数 -XX:+PrintEscapeAnalysis 来查看分析结果。
有了逃逸分析的支持后,用户可以使用参数 -XX:+EliminateAllocations 来开启标量替换,使用 +XX:+EliminateLocks 开启同步消除,使用参数 -XX:+PrintEliminateAllocations 查看标量的替换情况。