Java虚拟机在运行时会通过一系列优化手段提升程序执行效率,对象逃逸分析就是其中重要的一项,它直接决定了部分对象的分配位置,让不少未逃逸的对象可以直接在栈上创建,而不是传统的堆内存中。

什么是对象逃逸
对象逃逸指的是一个对象在方法中被创建后,其引用被传递到了方法外部,或者被其他线程访问到,导致对象的作用范围超出了当前方法的生命周期。如果对象仅在方法内部被使用,没有被外部引用持有,就称为对象未逃逸。
JVM通过逃逸分析来判断对象是否存在逃逸行为,分析过程会在JIT编译阶段进行,静态分析代码中对象的引用传递路径,确定对象的作用范围。
对象逃逸的判断场景
常见的对象逃逸场景有以下几种:
- 对象作为方法的返回值返回,外部方法可以拿到该对象的引用,属于逃逸
- 对象被赋值给类的成员变量或者静态变量,其他代码可以通过这些变量访问对象,属于逃逸
- 对象被传递到其他线程中,比如作为线程的构造参数,属于逃逸
- 对象仅在方法内部被使用,没有被传递到方法外,也没有被外部变量引用,属于未逃逸
为什么未逃逸对象可以在栈上创建
栈内存的生命周期和方法调用绑定,方法执行结束后栈帧就会出栈,栈上的内存会自动回收,不需要垃圾回收器介入。如果对象未逃逸,说明它的生命周期不会超过当前方法,那么就可以把对象直接分配到栈上,方法结束后内存自动释放,减少了堆内存的占用,也降低了垃圾回收的频率。
不过栈上分配并不是直接把对象的所有数据都放到栈里,JVM会采用标量替换的优化手段:如果对象可以拆解为基本类型的成员变量,就会把这些成员变量直接分配到栈的局部变量表中,不再创建完整的对象结构,进一步优化内存使用。
代码示例验证
下面通过一段代码来展示对象逃逸和栈上分配的效果,我们可以通过JVM参数来观察优化前后的差异:
public class EscapeAnalysisTest {
// 未逃逸的对象,仅在方法内部使用
public void noEscape() {
User user = new User();
user.age = 10;
// user对象没有传递到方法外,未逃逸
System.out.println(user.age);
}
// 逃逸的对象,作为返回值返回
public User hasEscape() {
User user = new User();
user.age = 20;
// user对象作为返回值,外部可以拿到引用,发生逃逸
return user;
}
static class User {
int age;
}
public static void main(String[] args) {
EscapeAnalysisTest test = new EscapeAnalysisTest();
// 循环调用未逃逸的方法,触发JIT优化
for (int i = 0; i < 1000000; i++) {
test.noEscape();
}
}
}
我们可以使用JVM参数-XX:+DoEscapeAnalysis开启逃逸分析(JDK1.7之后默认开启),-XX:-DoEscapeAnalysis关闭逃逸分析,分别运行程序,通过GC日志可以看到,开启逃逸分析后,堆上创建的对象数量会明显减少,因为未逃逸的User对象被优化到了栈上分配。
逃逸分析的优化限制
并不是所有未逃逸的对象都能被栈上分配,还需要满足以下条件:
- 对象不能被外部方法访问,也不能被其他线程共享
- 对象的大小较小,适合拆解到栈的局部变量表中
- 逃逸分析本身是静态分析,可能存在分析不准确的情况,部分未逃逸的对象也可能因为分析限制被分配到堆上
需要注意的是,栈上分配是JVM的优化手段,不是Java语言层面的特性,不同的JVM实现可能有不同的优化策略,开发者不需要主动去控制对象的分配位置,只需要了解其原理即可。