逃逸分析、栈上分配、标量替换

逃逸分析

逃逸分析是“一种确定指针动态范围的静态分析,它可以分析在程序的哪些地方可以访问到指针”。(出处参见【1】)

逃逸分析并不是直接优化代码的手段,而是为其它优化措施提供依据的分析技术。

逃逸分析的基本原理是:分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种称为方法逃逸;甚至还有可能被外部线程访问到,譬如赋值给可以在其它线程中访问的实例变量,这种称为线程逃逸;从不逃逸、方法逃逸到线程逃逸,称为对象由低到高的不同逃逸程度。

如果能证明一个对象不会逃逸到方法或线程之外(换句话说别的方法或线程无法通过任何途径访问到这个对象),或者逃逸程度比较低(只逃逸出方法而不会逃逸出线程),则可能为这个对象实例采取不同程度的优化,如:

  • 栈上分配(Stack Allocations):在 java 虚拟机中,java 堆上分配创建对象的内存空间几乎是 java 程序员都知道的常识,java 堆中的对象对于各个线程都是共享和可见的,只要持有这个对象的引用,就可以访问到堆中存储的对象数据。虚拟机的垃圾收集子系统会回收堆中不再使用的对象,但回收动作无论是标记筛选出可回收对象,还是回收和整理内存,都需要耗费大量资源。如果确定一个对象不会逃逸出线程之外,那让这个对象在栈上分配内存将会是一个很不错的注意,对象所占用的内存空间就可以随栈帧出栈而销毁。在一般应用中,完全不会逃逸的局部对象和不会逃逸出线程的对象所占的比例是很大的,如果能使用栈上分配,那大量的对象就会随着方法的结束而自动销毁了,垃圾收集子系统的压力将会下降很多。栈上分配可以支持方法逃逸,但不能支持线程逃逸。

由于实现起来需要更改大量假设了“对象只能堆分配”的代码,因此 HotSpot 虚拟机并没有采用栈上分配,而是使用了标量替换这么一项技术。

  • 标量替换(Scalar Replacement):若一个数据已经无法再分解成更小的数据来表示了,java 虚拟机中的原始数据类型(int、long 等数值类型及 reference 类型等)都不能再进一步分解了,那么这些数据就可以被称为标量。相对的,如果一个数据可以继续分解,那它就被称为聚合量(Aggregate),java 中的对象就是典型的聚合量。如果把一个对象拆散,根据程序的访问情况,将其用到的成员变量恢复为原始类型来访问,这个过程就被称为标量替换。假如逃逸分析能够证明一个对象不会被方法外部访问,并且这个对象可以被拆散,那么程序真正执行的时候将可能不去创建这个对象,而改为直接创建它的若干个被这个方法使用的成员变量来代替。由于该对象没有被实际分配,因此和栈上分配一样,它同样可以减轻垃圾回收的压力。将对象拆解后,除了可以让对象的成员变量在栈上(栈上存储的数据,很大机会被虚拟机分配至物理机器的高速寄存器中存储)分配和读写之外,还可以为后续进一步的优化手段创建条件。标量替换可以视作为栈上分配的一种特例,实现更简单(不用考虑整个对象完整结构的分配),但对逃逸程度的要求更高,它不允许对象逃逸出方法范围内。
  • 同步消除(Synchronization Elimination):也称锁消除,线程同步本身是一个相对耗时的过程,如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,那么这个变量的读写就肯定不会有竞争,对这个变量实施的同步措施也就可以安全的消除掉。

下面通过一系列的 Java 伪代码的变化过程来模拟逃逸分析是如何工作的,向读者展示逃逸分析能够实现的效果。初始代码如下:

// 完全未优化的代码
public int test(int x){
    int xx = x + 2;
    Point p = new Point(xx, 30);
    return p.getX();
}

此处省略了 Point 类的代码,这就是一个包含 x 和 y 坐标的 POJO 类型,读者应该很容易想象它的样子。

第一步,将 Point 的构造函数和 getX()方法进行内联优化(如果不清楚方法内联请先自行略微了解下):

// 构造函数内联后的样子
public int test(int x){
    int xx = x + 2;
    Point p = point_memory_alloc(); // 在堆中分配 p 对象的示意方法
    p.x = xx;						// Point 构造函数被内联后的样子
    p.y = 30;
    return p.x;						// Point::getX() 被内联后的样子
}

第二步,经过逃逸分析,发现在整个 test()方法的范围内 Point 对象实例不会发生任何程度的逃逸,这样可以对它进行标量替换优化,把其内部的 x 和 y 直接置换出来,分解为 test()方法内的局部变量,从而避免 Point 对象实例被实际创建,优化后的结果如下:

// 标量替换后的样子
public int test(int x){
    int xx = x + 2;
    int px = xx;
    int py = 30;
    return px;
}

第三步,通过数据流分析,发现 py 的值其实对方法不会造成任何影响,那就可以放心地去做无效代码消除得到最终优化结果,如下所示:

// 无效代码消除后的样子
public int test(int x){
    return x + 2;
}

但是在实际的应用程序中,尤其是大型程序中反而发现实施逃逸分析可能出现效果不稳定的情况,或分析过程耗时但却无法有效判别出非逃逸对象而导致性能(即时编译的收益)下降。

如果有需要,或者确认对程序运行有益,用户也可以使用参数 -XX:+DoEscapeAnalysis 来手动开启逃逸分析,开启之后可以通过参数 -XX:+PrintEscapeAnalysis 来查看分析结果。有了逃逸分析支持之后,用户可以使用参数 -XX:+EliminateAllocations 来开启标量替换,使用 -XX:+EliminateLocks 来开启同步消除,使用参数 -XX:PrintEliminateAllocations 查看标量的替换情况。

【1】:维基百科

参考《深入理解java虚拟机》和《深入拆解java虚拟机》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值