逃逸分析
概念
- 逃逸分析(Escape Analysis) 是目前
Java
虚拟机中比较前沿的优化技术, 它与类型继承关系分析一样, 并不是直接优化代码的手段, 而是为其他优化措施提供依据的分析技术 - 开启参数:使用参数
-XX: +DoEscapeAnalysis
来手动开启逃逸分析,-XX: +PrintEscapeAnalysis
来查看分析结果
基本原理
- 分析对象动态作用域, 当一个对象在方法里面被定义后, 它可能被外部方法所引用
类型
-
- 方法逃逸:作为调用参数传递到其他方法中
-
- 线程逃逸:可能被外部线程访问到, 譬如赋值给可以在其他线程中访问的实例变量
-
- 对象由低到高的不同逃逸程度:从不逃逸、 方法逃逸到线程逃逸
优化手段
-
- 栈上分配(支持方法逃逸)
- 概念:在
Java
虚拟机中,Java
是在堆上分配创建对象,Java
堆中的对象对于各个线程都是共享和可见的, 只要持有这个对象的引用, 就可以访问到堆中存储的对象数据。如果确定一个对象不会逃逸出线程之外, 那让这个对象在栈上分配内存,对象所占用的内存空间就可以随栈帧出栈而销毁 - 优化原因:虚拟机的垃圾收集子系统会回收堆中不再使用的对象, 但回收动作无论是标记筛选出可回收对象, 还是回收和整理内存, 都需要耗费大量资源,在一般应用中, 完全不会逃逸的局部对象和不会逃逸出线程的对象所占的比例是很大的, 如果能使用栈上分配, 那大量的对象就会随着方法的结束而自动销毁了, 垃圾收集子系统的压力将会下降很多
-
- 标量替换
- 概念:若一个数据已经无法再分解成更小的数据来表示了,
Java
虚拟机中的原始数据类型(int
、long
等数值类型及reference
类型等) 都不能再进一步分解了, 那么这些数据就可以被称为标量。 相对的, 如果一个数据可以继续分解, 那它就被称为聚合量(Aggregate
) ,Java
中的对象就是典型的聚合量。 如果把一个Java
对象拆散, 根据程序访问的情况, 将其用到的成员变量恢复为原始类型来访问, 这个过程就称为标量替换 - 过程:
- 1)假如逃逸分析能够证明一个对象不会被方法外部访问, 并且这个对象可以被拆散, 那么程序真正执行的时候将可能不去创建这个对象, 而改为直接创建它的若干个被这个方法使用的成员变量来代替
- 2)将对象拆分后, 除了可以让对象的成员变量在栈上(栈上存储的数据, 很大机会被虚拟机分配至物理机器的高速寄存器中存储) 分配和读写之外, 还可以为后续进一步的优化手段创建条件
- 开启参数:使用参数
-XX: +EliminateAllocations
来开启标量替换, 使用参数-XX: +PrintEliminateAllocations
查看标量的替换情况
注意:标量替换可以视作栈上分配的一种特例, 实现更简单(不用考虑整个对象完整结构的分配) , 但对逃逸程度的要求更高, 它不允许对象逃逸出方法范围内
-
- 同步消除
- 概念:线程同步本身是一个相对耗时的过程, 如果逃逸分析能够确定一个变量不会逃逸出线程, 无法被其他线程访问, 那么这个变量的读写肯定就不会有竞争,对这个变量实施的同步措施也就可以安全地消除掉
- 开启参数:使用
+XX: +EliminateLocks
来开启同步消除
优化例子
@Data
public class Point {
private int x, y;
private Point(int x, int y) {
this.x = x;
this.y = y;
}
}
//未优化方法
public int test(int x) {
int xx = x + 2;
Point p = new Point(xx, 42);
return p.getX();
}
//步骤1:构造函数内联后的样子
public int test(int x) {
int xx = x + 2;
// 在堆中分配P对象的示意方法
Point p = point_memory_alloc();
// Point构造函数被内联后的样子
p.x = xx;
// Point::getX()被内联后的样子
p.y = 42
return p.x;
}
// 步骤2: 标量替换后的样子
public int test(int x) {
int xx = x + 2;
int px = xx;
int py = 42
return px;
}
// 步骤3: 做无效代码消除后的样子
public int test(int x) {
return x + 2;
}
延伸:假如有人问,
Java
对象是否一定在堆中分配吗?答案:不是的,假如开启了逃逸分析,Java
虚拟机会进行逃逸分析,那么假如满足其条件便会出现栈上分配的情况