逃逸分析
- 分析变量能否逃出他的作用域
- 全局变量赋值逃逸
- 方法返回值逃逸
- 实例引用逃逸
- 线程逃逸
我们看下面一段代码:
这段代码演示了三种逃逸的场景。
public static SomeClass someClass;
//全局变量赋值逃逸
public void globalVariablePointerEscape() {
someClass = new SomeClass();
}
这段代码里,我们把一个局部变量赋值给了一个静态变量,局部变量的作用域是在方法内部,类变量的作用域是在类里面,所以作用域被放大了,显然发生了逃逸。
再看下面这段代码:
//方法返回值逃逸
// void someMethod(){
// SomeClass someClass = methodPointerEscape();
// }
public SomeClass methodPointerEscape(){
return new SomeClass();
}
methodPointerEscape方法里面,我们返回了一个对象,这个对象的作用域一开始也是在方法内部的,但是我们作为方法值返回了。那么这个时候,假设有另外一个方法,比如注释里的someMethod方法,调用了methodPointerEscape方法,那么someMethod方法里的someClass的作用域是在someMethod方法里面,所以new ComeClass()的作用域就从methodPointerEscape方法扩张到了someMethod方法。所以也发生了逃逸。
再看下面一段代码:
//实例引用传递逃逸
public void instancePassPointerEscape(){
this.methodPointerEscape()
.printClassName(this);
}
class SomeClass {
public void printClassName(EscapeTest escapeTest) {
System.out.println(escapeTest.getClass().getName());
}
}
这里的this传递给了printClassName方法,this的作用域原先是在EscapeTest实例下的,但是现在扩张到了SomeClass实例下面所以也发生了逃逸。
另外还有一个线程逃逸,并没有做代码示例,线程逃逸比较好总结。
当赋值给类变量或可以在其他线程中访问的实例变量就会发生线程逃逸。
逃逸状态标记
以上是逃逸的四种场景。JVM在做逃逸分析的时候,会针对这些场景做分析,分析完成之后,会用对象做一个逃逸状态的标记。
一个对象可以有三种逃逸状态的标记
- 全局级别逃逸:一个对象可能从方法或者当前线程中逃逸
也就是说其他的方法或者其他的线程也能够访问到这个对象。
* 对象作为方法的返回值返回
* 对象作为静态字段(static field)或者成员变量(field)
* 如果重写了某一个类的finalize()方法,那么这个类的对象都会被标记为全局逃逸状态并且一定会放在堆内存中。 - 参数级别逃逸
- 对象被作为参数传递给一个方法,但是在这个方法之外无法访问/对其他线程不可见。
- 无逃逸:一个对象不会逃逸
标量替换
标量:不能被进一步分解的量
* 基础数据类型
* 对象引用
聚合量:可以进一步分解的量
比如字符串就是一个可以分解的量。因为字符串是用字符数组实现的,可以分解。或者我们自己定义的变量,也都是聚合量。
什么是标量替换?
标量替换指: 通过逃逸分析确定该对象不会被外部访问,并且对象可以被进一步分解时,JVM不会创建该对象,而是创建他的成员变量来代替。
看下面这段代码:
如果开启了标量替换,并不会创建SomeTest这个实例,而是创建SomeTest成员变量去代替;
也就是说,开启标量替换之后,someTest方法里的代码就会被优化成
int age = 1;
int id = 1;
如果把对象进行标量替换后,原本的对象就不会分配内存空间了。
可以使用-XX:+EliminateAllocations
开启标量替换(JDK8 默认开启)
栈上分配
下面,我们聊一下栈上分配,我们都知道,java里面绝大多数对象都是存放在堆里面的,然后当对象没用的时候就会靠垃圾回收器去回收对象。
那么什么是栈上分配呢?
如果通过逃逸分析,能够确认对象不会被外部访问,就会在栈上分配对象。
如果在栈上分配对象的话,这个对象占用的空间就会在栈帧出栈的时候被销毁,所以,通过栈上分配可以降低垃圾回收的压力。