逃逸分析
什么是逃逸分析(Escape Analysis)
是一种算法,用来分析某个对象(变量)是否会发生逃逸
通俗的讲,在 JIT 编译过程中,发现某个对象它的动态作用域仅在某个方法中,其他的方法无法访问到这个变量(方法逃逸),其他的线程无法访问到这个变量(线程逃逸),这个对象不是全局变量(全局逃逸),那么这个变量就没发生逃逸
public class EscapeTest {
public static Object obj;
public void globalVariableEscape() { // 给全局变量赋值,发生逃逸
obj = new Object();
}
public Object methodEscape() { // 方法返回值,发生逃逸
return new Object();
}
public void instanceEscape() { // 实例引用发生逃逸
test(this);
}
}
优化措施
-
同步省略(锁消除):
在 JIT 编译过程中,如果发现一个对象不会被多线程访问,那么针对这个对象的同步措施就可以省略掉,即「锁销除」。例如 Vector 和 StringBuffer 这样的类,它们中的很多方法都是有锁的,当某个对象确定是线程安全的情况下,JIT编译器会在编译这段代码时进行锁销除来提升效率。 -
标量替换:
标量(Scalar)是指无法再分解成更小粒度的数据,例如 Java 中的原始数据类型(int,long等),相对如果一个数据可以继续分解,则称之为「聚合量(Aggregate)」,例如 Java对象。在 JIT 编译过程中,经过逃逸分析确定一个对象不会被其他线程或者方法访问,那么会将对象的创建替换成为多个成员变量的创建,称之为「标量替换」。标量替换减少了创建对象需要的堆内存,同时也不用进行 GC。 -
栈上分配:
栈上分配是指对象和数据不是创建在堆上,而是创建在栈上,随着方法的结束自动销毁。但实际上,JVM 例如常用的「HotSpot」虚拟机并没有实现栈上分配,实际是用「标量替换」代替实现的。
注意点
-XX:+DoEscapeAnalysis
开启逃逸分析-XX:+EliminateAllocations
开启标量替换;jdk1.8默认开启- 非堆上分配的空间要么存储在栈上,要么就在CPU寄存器中,这些都是相对稀缺的资源,因此逃逸分析和其它优化一样,(在实现上)肯定会面临妥协。HotSpot JVM上的一个默认限制是大于64个元素的数组不会进行逃逸分析优化。这个大小可以通过启动参数
-XX:EliminateAllocationArraySizeLimit=n
来进行控制,n是数组的大小。
逃逸分析优缺点
- 分析是会耗时的;极端情况下,如果没有可优化的代码块,那么就是负提升
- 标量替换和栈上分配都是为了减少在堆中创建对象,这样就减少了GC频率,从而提升性能
- 因为获取锁资源是很耗时的,同步省略(锁消除)相当于直接去掉了锁操作,这样大大提升性能;
测试代码
jvm参数:
-server -Xmx500m -Xms500m -Xss100m -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+EliminateAllocations -XX:+EliminateLocks
参数解释,开启server模式,设置堆初始和最大空间500M,栈空间100M,开启逃逸分析,开启GC日志打印功能,开启标量替换,开启锁消除
public class MyTest{
private static void allot() {
// 大于65,不会被优化,会被放入堆中
byte[] b = new byte[65];
// 测试锁消除
synchronized (b) { //同步代码块
b[0]=1;
}
}
public static void main(String[] args) throws InterruptedException {
long t1 = System.currentTimeMillis();
for (int i = 0; i < 100000000; i++) {
allot();
}
long t2 = System.currentTimeMillis();
System.out.println("耗时:"+(t2-t1));
try {
Thread.sleep(50000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 当开启逃逸分析后,byte[65]不会被优化,会看到打印结果耗时1354毫秒; 而byte[64]会被优化,耗时4毫秒,差距非常大