先说结论:在默认情况下,Java对象的分配是在堆中进行的,但在经过逃逸分析判断后,确定某个对象不会逃出当前方法(即方法内使用后没有任何外部引用),那么JVM会将该对象分配到栈上。栈上分配的对象在方法结束时会自动销毁,避免了堆上分配所需的垃圾回收过程,从而提高性能。
那么什么是逃逸分析?
逃逸分析是Java虚拟机的一项优化技术,它主要用于判断一个对象的使用范围。如果确定一个对象不会逃离其所在的方法或线程,那么JVM可以对该对象进行优化,比如将它分配在栈上而不是堆上,以减少垃圾回收的负担。
1. 方法逃逸(Method Escape)
定义:方法逃逸是指一个对象在方法中被创建,但这个对象被传递给了其他方法或返回给了调用者。这意味着对象有可能在方法结束后仍然被外部代码引用。
举例:
public class MethodEscapeExample {
public Object createObject() {
Object obj = new Object(); // 创建了一个对象
return obj; // 返回给调用者,可能被外部代码继续引用
}
public void anotherMethod() {
Object obj = createObject();
// `obj`对象在`createObject`方法中被创建,并返回给了`anotherMethod`方法使用
}
}
在这个例子中,obj对象在createObject()方法中被创建,但它被返回给了调用者(即anotherMethod()方法)。这意味着obj对象的生命周期不仅限于createObject()方法,外部方法也可以继续使用它。因此,这个对象逃逸出了它的创建方法。
影响:由于对象可能被外部引用,所以不能将它分配到栈上,而是必须分配到堆中,因为堆内存是线程共享的。只有在堆上分配,外部方法或调用者才能在方法执行完毕后继续访问这个对象。
2. 线程逃逸(Thread Escape)
定义:线程逃逸是指一个对象在创建后,被另一个线程所引用,从而导致对象逃逸出当前线程的控制范围。这种对象可能被多个线程共享,因此需要分配在堆中,并且需要考虑线程安全性问题。
举例:
public class ThreadEscapeExample {
private static Object sharedObject;
public void createObjectInThread() {
Object obj = new Object(); // 创建了一个对象
new Thread(() -> {
sharedObject = obj; // 对象被另一个线程引用,发生线程逃逸
}).start();
}
}
在这个例子中,obj对象在createObjectInThread()方法中被创建,但它被传递给了一个新的线程,并且被保存到sharedObject变量中。因此,obj对象不仅逃逸出了创建它的方法,还逃逸出了当前线程,成为了另一个线程的共享对象。
影响:线程逃逸的对象可能会被多个线程并发访问,必须考虑到线程安全性问题。由于它需要在不同线程间共享,因此必须分配到堆中,而不能分配到栈上。JVM也不能对这种对象进行栈上分配的优化。
总结
- 方法逃逸是对象超出了创建它的方法的作用域,可能被外部方法引用,因此不能分配在栈上,而必须分配在堆中。
- 线程逃逸是对象逃逸出了创建它的线程,被其他线程引用并共享,这不仅要求对象在堆中分配,还可能需要采取额外的线程安全措施。
示例代码说明
以下代码演示了可能发生的栈上分配场景:
public class EscapeAnalysisTest {
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 50000000; i++) {
allocate();
}
System.out.println((System.currentTimeMillis() - start) + " ms");
}
static void allocate() {
Object obj = new Object(); // 创建了一个对象
}
}
在这个示例中,Object obj是在allocate()方法中创建的对象,由于obj对象只在该方法中使用,且不会被传递给其他方法或线程,因此**obj属于不可逃逸的对象**。
通过逃逸分析,JVM可以判断obj对象不会离开当前方法的作用域,因此在编译时(JIT编译器进行优化时),JVM可以选择将obj对象分配到栈上。这意味着,当方法执行完毕后,obj的内存空间会自动释放,无需垃圾回收器的参与,从而提升程序的性能。
总结
栈上分配:如果JVM通过逃逸分析确定某个对象不会逃出当前方法(即方法内使用后没有任何外部引用),那么JVM会将该对象分配到栈上。栈上分配的对象在方法结束时会自动销毁,避免了堆上分配所需的垃圾回收过程,从而提高性能。
堆上分配:如果对象存在方法逃逸或线程逃逸的可能,JVM仍会将该对象分配到堆中,因为堆上的对象可以被多个方法或线程共享,且垃圾回收器能够管理这些对象的生命周期。