在java中,我们创建的对象一般都是存放在堆内存当中的,堆内存不足时,GC会对堆内存进行垃圾回收,如果GC次数过于频繁,这就会影响程序的性能,所以这才有了逃逸分析,逃逸分析的目的就是判断哪些对象是可以存放在栈内存当中,而不用存放在堆内存当中的,从而让其随着线程的消逝而消逝,进而减少GC发生的频率。
1.定义
逃逸分析总得来说就是,jvm可以分析新创建对象的使用范围,并决定是否在java堆上分配内存的一项技术。在方法中创建对象之后,如果这个对象除了在本方法体中还在其她地方被引用了,此时如果该方法执行结束,由于该对象还有其它地方被引用着,那么GC是不能回收该对象的,此时便称为内存逃逸现象。(逃逸,你可以理解成这个对象逃逸出这个方法体)。一个对象如果并没有逃逸出方法体,那么就有可能被优化成栈上分配,这样就无需在堆上分配内存,也无需进行垃圾回收了(因为方法运行结束,方法对应的栈帧就出栈了,栈帧里面的内容就销毁了,这个分配到该栈帧的对象就消失了,不用像堆内存那样需要GC了)。
2.如何开启逃逸分析
‐XX:+DoEscapeAnalysis //表示开启逃逸分析 (jdk1.8默认开启),虚拟机将尝试在栈内存分配未逃逸的对象,减少堆内存使用并优化垃圾回收(GC)。
‐XX:‐DoEscapeAnalysis //表示关闭逃逸分析,所有对象默认分配到堆内存,栈内存不参与对象分配。
3.逃逸分析的类型
3.1 方法逃逸
如果方法内的对象被方法外的代码引用,则该对象发生方法逃逸。代码示例:
public class MethodEscape {
private Object obj;
public void methodEscape() {
//对象逃逸到当前方法外
obj = new Object();
}
}
3.2线程逃逸
如果对象被当前线程以外的代码引用,则该对象发生线程逃逸。代码示例:
public class ThreadEscape {
public void threadEscape(){
//这里新创建了一个线程
new Thread(() -> {
//对象逃逸到当前线程外
System.out.println(new Object());
}).start();
}
}
4.逃逸分析的优化方式
4.1栈上分配
如果一个对象没有发生逃逸,则可以将该对象分配在栈上。这样做的好处就是减少堆内存分配,降低垃圾回收的压力。代码示例:
public void stackAlloc(){
//对象没有逃逸,将对象分配在栈上
Object obj = new Object();
}
我们再来看一些代码例子:
public class EscapeAnalysis {
public static Object globalObject;
public Object obj;
public void globalEscape(){
globalObject = new Object(); // globalObject为静态变量,外部线程可见,发生逃逸
}
public void objEscape(){
obj = new Object(); // 赋值给堆中实例字段,外部线程可见,发生逃逸
}
public Object returnObjectEscape(){
return new Object(); // 返回实例,外部线程可见,发生逃逸
}
public void noEscape(){
Object noEscape = new Object(); // 仅创建线程可见,对象无逃逸
}
}
5.逃逸分析测试
我们写一个测试代码,for循环一亿次,循环体内调用allot()方法,allot()方法内部就写了个new Student对象的逻辑,这个Student对象是内部的,没有逃逸,所以从理论上来说JVM会进行优化。我们来对比一下开启和关闭逃逸分析之后各自程序的运行时间:
5.1开启逃逸分析
因为JDK1.8是默认开启逃逸分析的,所以直接运行程序即可。
5.2关闭逃逸分析
总结:无论是从代码的执行时间,还是从堆内存中对象的数量(十三万个 VS 一千多万个)来分析,在上述场景下,开启逃逸分析都是有正向收益的。