
JVM运行时数据区详解变量、对象和类信息到底存在哪里写 Java 代码时我们通常只关注变量的值和对象的行为很少去思考它们在 JVM 中到底如何存放。inta10;程序运行之后a并不是一直存在于.class文件中而是会随着方法执行进入 JVM 的运行时数据区。再比如ObjectobjnewObject();这里既有变量引用obj也有通过new创建的对象实例它们的生命周期和存储位置并不相同。理解 JVM 运行时数据区可以帮助我们搞清楚几件事局部变量为什么方法结束后自动消失对象为什么需要 GC 回收静态变量、类信息、对象实例分别存放在哪里。这也是理解 Java 内存模型和垃圾回收机制的基础。目录全景图五个区域各司其职程序计数器虚拟机栈本地方法栈堆方法区实战变量去哪了五个区域的差异小结全景图五个区域各司其职JVM 规范把运行时内存划分为五个区域。从线程的角度看可以分成两类线程私有每个线程独占一份线程创建时分配线程销毁时回收程序计数器虚拟机栈本地方法栈线程共享所有线程共用JVM 启动时创建堆方法区注意静态变量类字段在 HotSpot JDK 8 中随 Class 对象存放在堆中而非方法区。程序计数器这是五个区域里最小的一块也是唯一不会抛 OOM 的区域。程序计数器保存当前线程下一条需要执行的字节码地址。JVM 的执行引擎执行完一条字节码后会从程序计数器取出下一条的行号继续执行。每个线程都有自己的程序计数器互不干扰。这也是线程私有的含义——线程 A 执行到第 10 行线程 B 可能执行到第 50 行各自记录互不影响。可以把它理解成书签。你在一本书里读到第 30 页夹了个书签别人读到第 80 页也夹了个书签。书签各自独立标记的是各自读到哪了。唯一需要注意的如果当前执行的是 native 方法程序计数器的值为空Undefined。因为 native 方法的执行交给操作系统了JVM 管不着。虚拟机栈虚拟机栈是我们最常接触的内存区域也是理解 Java 方法调用的关键。每当一个方法被调用JVM 会为这个方法创建一个栈帧Stack Frame压入虚拟机栈。方法执行完毕栈帧弹出。一个线程在同一时刻只有栈顶的栈帧是活跃的——正在执行的那个方法。栈帧里装什么每个栈帧包含四个部分组成说明局部变量表存方法的局部变量和方法参数。基本类型直接存值引用类型存引用指针操作数栈方法执行过程中的临时计算空间比如做加法时先 push 两个数再执行 iadd动态链接指向运行时常量池中该方法的引用支持多态方法返回地址方法执行完后回到调用者的哪一行继续执行其中最重要的是局部变量表。你写的每一个局部变量不管是int a 10还是String s hello都存在这里。基本类型存的是值本身引用类型存的是指向堆中对象的引用。方法调用时创建栈帧方法结束时栈帧销毁。局部变量的生命周期和方法调用一样长——方法返回栈帧弹出局部变量自然就没了。两种异常虚拟机栈有两种出错场景StackOverflowError方法调用太深栈帧不断压入栈空间撑爆了。最常见的原因是递归没有终止条件。publicclassStackOverflowDemo{publicstaticvoidmain(String[]args){recurse(0);}staticvoidrecurse(intdepth){// 每调一次压入一个栈帧直到栈空间不够recurse(depth1);}}// 运行结果java.lang.StackOverflowErrorOutOfMemoryError如果虚拟机栈支持动态扩展扩展时内存不够了就会抛这个错。不过现在的 JVM 实现通常在创建线程时就固定了栈大小所以更多见的是 StackOverflowError。本地方法栈本地方法栈和虚拟机栈几乎一样区别在于它服务的是native 方法——用 C/C 写的方法通过 JNI 调用。比如Thread.currentThread()、Object.hashCode()这些方法底层都是 native 实现。调用这些方法时压入的是本地方法栈不是虚拟机栈。在 HotSpot JVM 的实现里本地方法栈和虚拟机栈合二为一了不单独区分。所以面试的时候提一句本地方法栈为 native 方法服务就够了。堆堆是 JVM 中用于存放对象实例和数组的主要区域也是 GC 主要工作的地方。所有对象实例和数组都在堆上分配。你每次写new Object()、new int[100]对象的内存都来自堆。前面说的虚拟机栈里存的是引用真正的对象本体在堆里。来看一个简单的例子publicvoidcreate(){ObjectobjnewObject();}调用create()时发生了两件事在虚拟机栈的栈帧中局部变量obj存了一个引用指针在堆中分配了一块内存存放Object实例obj是引用存在栈里对象本体存在堆里。方法结束后栈帧销毁obj这个引用就没了。但对象还在堆里直到 GC 发现它没有引用指向了才会回收。为什么需要 GCC 里你new了一个对象用完之后必须自己delete不然内存就泄漏了。Java 把这个责任交给了 GC垃圾回收你只管newGC 帮你判断哪些对象没人用了然后回收它们的内存。GC 判断对象没人用的依据是可达性分析。从一组叫GC Roots的根节点出发比如虚拟机栈里的局部变量、类的静态字段沿着引用链往下找能到达的对象就是活的到达不了的就是死的可以回收。GC Roots │ ├── 虚拟机栈中的局部变量 │ └── obj ──→ Object实例可达不回收 │ └── 类的静态字段 └── list ──→ ArrayList实例可达不回收 └── 其中引用的对象可达不回收 堆中没有任何引用指向的对象 → 不可达GC 回收这里先关注对象为什么需要 GC具体的垃圾回收算法和分代设计会在后面的 GC 章节展开。方法区方法区存储的是类的元信息类名、字段信息、方法信息、方法的字节码、运行时常量池。当你写了一个class UserJVM 第一次用到这个类时会通过类加载器把.class文件读进来在方法区中创建这个类的元数据。之后你每次new User()堆里分配的是对象实例但User 这个类长什么样的信息始终在方法区里。类加载完成后JVM 会保存对应的类元信息。之后无论创建多少个对象实例这些实例共享同一份类定义。永久代和元空间方法区是一个逻辑概念JVM 规范定义了它但不同版本的 HotSpot JVM 实现方式不同JDK 7 及之前用永久代PermGen实现放在 JVM 堆内存里大小固定容易 OOMJDK 8 起改为元空间Metaspace使用本地内存操作系统内存默认不限大小永久代的问题在于它属于 JVM 堆内存中的一块固定区域类信息增长后容易出现空间不足。JDK 8 引入元空间后将类元数据放到了本地内存中减少了固定大小带来的限制。字符串常量池也从永久代移到了堆里这是 JDK 7 就完成的变更。实战变量去哪了来看一段代码分析每个变量在运行时存在哪个区域publicclassMemoryDemo{// 静态字段属于类变量// HotSpot JDK8 中存放在堆中的 Class 对象相关区域privatestaticintcount0;publicstaticvoidmain(String[]args){// args 引用 → 虚拟机栈main 方法栈帧// args 数组对象 → 堆count;createObject();}publicstaticObjectcreateObject(){// 局部变量 a → 虚拟机栈createObject 方法栈帧inta10;// obj 引用 → 虚拟机栈// Object 实例 → 堆ObjectobjnewObject();returnobj;}}逐个拆解变量存储位置说明count堆Class 对象相关区域静态字段属于类变量HotSpot JDK8 中随 Class 对象存放在堆中args引用虚拟机栈main 方法的局部变量args数组对象堆通过new String[]创建a虚拟机栈createObject 方法的局部变量基本类型直接存值obj引用虚拟机栈createObject 方法的局部变量存的是指针Object 实例堆通过new创建的对象一个规律基本类型的局部变量直接存在栈帧的局部变量表里方法结束就没了。对象的引用存在栈帧里但对象本体在堆里方法结束后引用没了对象等着 GC 来回收。五个区域的差异区域线程归属存储内容是否 GC可能的异常程序计数器私有字节码行号否无虚拟机栈私有栈帧局部变量、操作数栈否StackOverflowError / OOM本地方法栈私有native 方法栈帧否StackOverflowError / OOM堆共享对象实例、数组是OOM: Java heap space方法区共享类信息、常量池是类卸载OOM: Metaspace记住这张表面试时问堆和栈的区别、“哪些区域会 GC”、“哪些会抛 OOM”都能从这里找到答案。小结JVM 将不同生命周期的数据放入不同区域方法调用产生的数据随着栈帧销毁对象实例交给 GC 管理类元信息则伴随类加载存在。但分区只是静态的内存模型。堆里的对象创建了什么时候算没人用了GC 用什么算法判断可达性分代模型是怎么回事——这些问题是下一篇的事了。