先以官方的形式来说下什么是逃逸分析。
逃逸分析就是:一种确定指针动态范围的静态分析,它可以分析在程序的哪些地方可以访问到指针。
在JVM的即时编译语境下,逃逸分析将判断新建的对象是否逃逸。
即时编译判断对象是否逃逸的依据:
一种是对象是否被存入堆中(静态字段或者堆中对象的实例字段),另一种就是对象是否被传入未知代码。
分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种称为方法逃逸;甚至还有可能被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸;从不逃逸、方法逃逸到线程逃逸,称为对象由低到高的不同逃逸程度。
逃逸分析的类型有两种:
方法逃逸
线程逃逸
什么是:方法逃逸(对象逃出当前方法):
当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其它方法中。
什么是:线程逃逸((对象逃出当前线程):
这个对象甚至可能被其它线程访问到,例如赋值给类变量或可以在其它线程中访问的实例变量
当一个对象在方法里面被定义后,它可能被外部方法所引用,这种称为方法逃逸
方法逃逸包括:
通过调用参数,将对象地址传递到其他方法中,
对象通过return语句将对象指针,返回给其他方法
等等
我们可以用下面的代码来表示这个现象。
//StringBuffer对象发生了方法逃逸
public static StringBuffer createStringBuffer(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb;
}
上面的例子中,StringBuffer 对象通过return语句返回。
StringBuffer sb是一个方法内部变量,上述代码中直接将sb返回,这样这个StringBuffer有可能被其他方法所改变,这样它的作用域就不只是在方法内部,虽然它是一个局部变量,称其逃逸到了方法外部。
甚至还有可能被外部线程访问到,譬如赋值给类变量或可以在其他线程中访问的实例变量,称为线程逃逸。
不直接返回 StringBuffer,那么StringBuffer将不会逃逸出方法。
具体的代码如下:
// 非方法逃逸
public static String createString(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1);
sb.append(s2);
return sb.toString();
}
可以看出,想要逃逸方法的话,需要让对象本身被外部调用,或者说, 对象的指针,传递到了 方法之外。
当一个对象可能被外部线程访问到,这种称为线程逃逸。
例如赋值给类变量或可以在其它线程中访问的实例变量
同步消除:如果一个对象被逃逸分析发现只能被一个线程所访问,那对于这个对象的操作可以不同步。
栈上分配:如果确定一个对象不会逃逸出线程之外,那让这个对象在栈上分配内存将会是一个很不错的主意,对象所占用的内存空间就可以随栈帧出栈而销毁。
标量替换:如果一个对象被逃逸分析发现不会被外部方法访问,并且这个对象可以拆散,那么程序真正执行的时候将可能不去创建这个对象,而改为直接创建它的若干个比这个方法使用的成员变量来代替。将对象拆分后,可以让对象的成员变量在栈上分配和读写。
JVM中通过如下参数可以指定是否开启逃逸分析:
-XX:+DoEscapeAnalysis
:表示开启逃逸分析(JDK 1.7之后默认开启)。
-XX:-DoEscapeAnalysis
:表示关闭逃逸分析。
如果一个对象不再方法体内,或者线程内发生逃逸:
1、栈上分配:
一般情况下,不会逃逸的对象所占的空间比较大,如果能使用栈上的空间,那么大量的对象将随方法的结束而销毁,减少了GC的压力。
2、同步销毁:
如果你定义的类的方法有同步锁,但在运行时,却只有一个线程在访问,此时逃逸分析后的机器码,会去掉同步锁运行
3、标量替换
java虚拟机中的原始类型(int,long等)都不能再进一步分解,称他们为标量。相对的,如果一个数据可以继续分解,称它为聚合量,java中最典型的聚合量是对象。如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以分解,那程序真正执行的时候可以不创建这个对象,而直接创建它的若干个被这个方法使用的成员变量。拆算后的变量便可以被单独分析和优化,可以各自分别在栈帧和寄存器上分配空间,原本的对象无需整体分配空间了。