写在前面
本文隶属于专栏《100个问题搞定Java虚拟机》,该专栏为笔者原创,引用请注明来源,不足和错误之处请在评论区帮忙指出,谢谢!
本专栏目录结构和参考文献请见100个问题搞定Java虚拟机
解答
逃逸分析是一种确定对象动态作用域的技术。
如果能证明一个对象不会逃逸到方法或线程之外,也就是别的方法或线程无法通过任何途径访问到这个对象,则可以为这个变量进行一些高效的优化。
栈上分配、标量替换和同步消除(又叫锁消除),正是基于逃逸分析结果的优化手段。
补充
逃逸分析
逃逸分析是一种确定指针动态范围的静态分析,它可以分析在程序的哪些地方可以访问到指针。
在 Java 虚拟机的即时编译语境下,逃逸分析将判断新建的对象是否逃逸。
即时编译器判断对象是否逃逸的依据,一是对象是否被存入堆中(静态字段或者堆中对象的实例字段),二是对象是否被传入未知代码中。
前者很好理解:一旦对象被存入堆中,其他线程便能获得该对象的引用。即时编译器也因此无法追踪所有使用该对象的代码位置。
关于后者,由于 Java 虚拟机的即时编译器是以方法为单位的,对于方法中未被内联的方法调用,即时编译器会将其当成未知代码,毕竟它无法确认该方法调用会不会将调用者或所传入的参数存储至堆中。
因此,我们可以认为方法调用的调用者以及参数是逃逸的。
通常来说,即时编译器里的逃逸分析是放在方法内联之后的,以便消除这些“未知代码”入口。
方法逃逸和线程逃逸
当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,称为方法逃逸。
甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。
栈上分配
Java虚拟机中,在Java堆上分配创建对象的内存空间几乎是Java程序员都清楚的常识了,Java堆中的对象对于各个线程都是共享和可见的,只要持有这个对象的引用,就可以访问堆中存储的对象数据。
虚拟机的垃圾收集系统可以回收堆中不再使用的对象,但回收动作无论是筛选可마收对象,还是回收和整理内存都需要耗费时间。
如果确定一个对象不会逃逸出方法之外,那让这个对象在栈上分配内存将会是一个很不错的主意,对象所占用的内存空间就可以随栈帧出栈而销毁在一般应用中,不会逃逸的局部对象所占的比例很大,如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁了,垃圾收集系统的压力将会小很多。
不过,由于实现起来需要更改大量假设了“对象只能堆分配”的代码,因此 Hotspot虚拟机并没有采用栈上分配,而是使用了标量替换这么一项技术。
标量替换
原本需要在内存中连续分布的对象,被拆散为一个个单独的字段(JVM 的基本数据类型)。
这些字段既可以存储在栈上,也可以直接存储在寄存器中。而该对象的对象头信息则直接消失了,不再被保存至内存之中。
由于该对象没有被实际分配,因此和栈上分配一样,它同样可以减轻垃圾回收的压力。
与栈上分配相比,它对字段的内存连续性不做要求,而且,这些字段甚至可以直接在寄存器中维护,无须浪费任何内存空间。
同步消除(锁消除)
如果即时编译器能够证明锁对象不逃逸,那么对该锁对象的加锁、解锁操作没有意义。
这是因为其他线程并不能获得该锁对象,因此也不可能对其进行加锁。
在这种情况下,即时编译器可以消除对该不逃逸锁对象的加锁、解锁操作。
实际上,传统编译器仅需证明锁对象不逃逸出线程,便可以进行锁消除。
由于 Java 虚拟机即时编译的限制,上述条件被强化为证明锁对象不逃逸出当前编译的方法。
synchronized (new Object()) {
}
上面的Java代码会被完全优化掉。这正是因为基于逃逸分析的锁消除。
详情可以查看我的这篇博客——
由于其他线程不能获得该锁对象,因此也无法基于该锁对象构造两个线程之间的 happens-before 规则。
synchronized (escapedObject) {
}
这段代码则不然。
由于其他线程可能会对逃逸了的对象escapedObject进行加锁操作,从而构造了两个线程之间的 happens-before 关系。
因此即时编译器至少需要为这段代码生成一条刷新缓存的内存屏障指令。
部分逃逸分析
C2 的逃逸分析与控制流无关,相对来说比较简单。Graal 则引入了一个与控制流有关的逃逸分析,名为部分逃逸分析(partial escape analysis)。
它解决了所新建的实例仅在部分程序路径中逃逸的情况。
在某些情况下,比如 if 语句中创建了一个新对象,假设 if 语句的条件成立的可能性只有 1%,那么在 99% 的情况下,程序没有必要新建对象。
部分逃逸分析将根据控制流信息,判断出新建对象仅在部分分支中逃逸,并且将对象的新建操作推延至对象逃逸的分支中。
这将使得原本因对象逃逸而无法避免的新建对象操作,不再出现在只执行 if-else 分支的程序路径之中。
与 C2 所使用的逃逸分析相比,Graal 所使用的部分逃逸分析能够优化更多的情况,不过它编译时间也更长一些。