当实例对象存储在堆区时:实例对象存放在堆区,对实例的引用存在线程栈上,而实例的元数据存在方法区或者元空间。
那么,实例对象一定是存放在堆区吗?答案是不一定,如果实例对象没有发生线程逃逸行为,就会被存储在线程栈中。这涉及到JIT在编译中对Java代码的优化——逃逸分析。
逃逸分析的基本行为就是分析对象动态作用域。
如果一个对象在方法中被定义,但是对象的使用仅是在当前方法中,而且对象本身比较简单,那么对象就有可能被存储在线程栈中。
使用逃逸分析,编译器可以对代码做如下优化:
- 同步省略:如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可 以不考虑同步。
- 将堆分配转化为栈分配:如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
- 分离对象或标量替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。
/**
* 对象逃逸测试
*/
public class StackAllocTest {
/**
* 进行两种测试:
* 关闭逃逸分析,同时调大堆空间,避免对被GC的发生,如果由GC信息将会被打印出来
* VM运行参数: -Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
*
* 开启逃逸分析
* VM运行参数: -Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
*
* 执行main方法后
* jps查看进程
* jmap -histo 进程ID
*/
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 500000; i++) {
alloc();
}
long end = System.currentTimeMillis();
// 查看执行时间
System.out.println("cost-time " + (end - start) + " ms");
try {
Thread.sleep(100000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static void alloc() {
// JIT在编译时,会对代码进行逃逸分析
Student student = new Student();
}
static class Student {
private String name;
private int age;
}
}
在执行main函数时,设定是否启用逃逸分析(从jdk1.7开始,默认开启逃逸分析)。通过查看堆内存中Student对象的个数判断对象实例是否全部存储在堆上。
在关闭逃逸分析后,看到对象实例为500000个。
开启逃逸分析,发现堆中的实例对象只有8万多个,可见很大一部分实例被存放在了线程栈上