Java 之逃逸分析
一般情况下,Java 源代码需要经过前端编译器(比如 javac)编译成 class 文件,然后再通过解释器解释执行。但是对于一些热点代码(频繁调用的方法或代码块),虚拟机为了提高它们的执行效率,会在运行期将这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器就是即时编译器(Just In Time Compiler,JIT),比如 HotSpot 中的 C1、C2 编译器。
在 JIT 进行优化的过程中,逃逸分析为这些优化手段提供了重要的依据。逃逸分析的基本行为就是分析对象的动态作用域,当一个对象在方法中被定义后,它可能被外部方法所引用,比如作为调用参数传递到其他方法中,称为方法逃逸。有的还有可能被外部线程访问到,比如赋值给类变量或者其他线程中的实例变量,这种被称为线程逃逸。
1 | public class EscapeDemo { |
我们知道,栈帧是线程私有的。每个方法在执行时都会创建一个栈帧用来存储局部变量表、操作数栈等信息,每个方法从调用到结束的过程,就是一个栈帧在虚拟机从入栈到出栈的过程。方法的参数以及在方法中定义的变量都是局部变量,基本类型的变量存放的是对应类型的值,而引用类型的变量存放的则是对象的引用,也就是地址值,对象本身会在堆中分配。因此,如果局部变量是一个引用类型的变量,那么只要它的引用被传递到了外界,就有可能引发线程安全问题。如果能够证明一个对象不会逃逸到方法或者线程之外,也就是别的方法或者线程无法通过任何途径访问到这个对象,那么就可以对这个变量进行一些高效的优化,比如栈上分配、同步消除、标量替换等。
Java 堆上分配对象的内存空间已经成为常识,堆中的对象对于各个线程都是共享和可见的,只要持有这个对象的引用,就可以访问堆中存储的这个对象的数据。虚拟机的垃圾收集可以回收堆中不再使用的对象,但是无论是筛选可回收对象还是回收、整理内存都需要耗费时间。如果确定一个对象不会逃逸到方法之外,那么就可以让这个对象在栈上分配内存,对象所占用的内存空间就可以随着栈帧出栈而销毁。
线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸到当前线程之外,无法被其他线程访问到,那么这个变量的读写肯定不会存在竞争,那么对该变量实施的同步就可以消除掉,也就是锁消除。
1 | public String lock_elision() { |
Java 虚拟机中的原始数据类型(int、long、reference 等)都是标量(无法再分解成更小的数据来表示)。相对的,如果一个数据可以继续分解,那么它就称作聚合量。Java 中最典型的聚合量就是对象。如果将一个对象拆散,根据程序访问的情况,将其使用到的成员变量恢复成原始类型来访问,这就是标量替换。如果逃逸分析证明一个对象不会被外部访问到,并且这个对象可以拆分,那么程序在真正执行的时候可能并不会创建该对象,而是改为直接创建它的若干个被这个方法使用到的成员变量来替代。将对象拆分后,除了可以让对象的成员变量在栈上分配和读写之外,还可以为后续的进一步优化创造条件。
参考
《深入理解 Java 虚拟机:JVM 高级特性与最佳实践》