当实例对象存储在堆区时:实例对象存放在堆区,对实例的引用存在线程栈上,而实例的元数据存在方法区或者元空间。

那么,实例对象一定是存放在堆区吗?答案是不一定,如果实例对象没有发生线程逃逸行为,就会被存储在线程栈中。这涉及到JIT在编译中对Java代码的优化——逃逸分析。

逃逸分析的基本行为就是分析对象动态作用域。
如果一个对象在方法中被定义,但是对象的使用仅是在当前方法中,而且对象本身比较简单,那么对象就有可能被存储在线程栈中。

使用逃逸分析,编译器可以对代码做如下优化:

  1. 同步省略:如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可 以不考虑同步。
  2. 将堆分配转化为栈分配:如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
  3. 分离对象或标量替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在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对象的个数判断对象实例是否全部存储在堆上。

JAVA实例方法直接调用实例方法 java实例方法存储在哪里_System


在关闭逃逸分析后,看到对象实例为500000个。

开启逃逸分析,发现堆中的实例对象只有8万多个,可见很大一部分实例被存放在了线程栈上

JAVA实例方法直接调用实例方法 java实例方法存储在哪里_堆区_02