跳到主要内容

JVM 运行时数据区域

JVM 规范把运行时状态分为程序计数器、虚拟机栈、堆、方法区、运行时常量池和本地方法栈。理解这些区域的目的,是根据数据的生命周期和共享范围判断问题,而不是背一张固定的 HotSpot 内存图。

1. 程序计数器记录当前执行位置

每个线程都有自己的程序计数器。线程执行非 native 方法时,它记录当前 JVM 指令的位置;执行 native 方法时,其值不由 JVM 规范规定。

线程切换后能够继续执行,依赖虚拟机保存和恢复包括执行位置在内的线程状态。程序计数器很小,也没有在 JVM 规范中规定对应的内存溢出异常。

2. 虚拟机栈保存方法调用状态

每个线程创建时拥有一个 JVM 栈。每次方法调用创建一个栈帧,方法正常返回或异常退出后销毁该帧。

栈帧主要包含:

  • 局部变量表。
  • 操作数栈。
  • 当前类运行时常量池的引用,用于动态链接。
  • 方法返回和异常处理所需状态。
static long sum(int left, int right) {
long result = (long) left + right;
return result;
}

参数与局部变量会占用局部变量表槽位,计算过程使用操作数栈。具体栈帧内存布局由 JVM 实现决定,不应把逻辑槽位直接等同为固定字节数。

递归过深通常产生 StackOverflowError。创建大量平台线程失败则可能表现为 OutOfMemoryError: unable to create native thread,它还受到进程内存、系统线程限制和容器配额影响。

3. 堆保存自动管理的对象

堆由所有 JVM 线程共享,是垃圾收集器管理的主要区域。规范要求对象和数组的存储从堆分配,但不规定堆必须连续,也不规定必须使用新生代、老年代或某种收集器。

HotSpot 可能通过逃逸分析和标量替换消除某些对象分配。此时从 Java 语义上仍然像创建了对象,但运行时不一定留下一个可观察的完整堆对象。

-Xms-Xmx 用于设置 HotSpot 堆的初始和最大大小。判断容量时要区分:

  • reserved:保留的虚拟地址范围。
  • committed:已承诺可供 JVM 使用的内存。
  • used:当前已使用空间。

4. 方法区保存每个类型的运行时结构

方法区由线程共享,保存每个类或接口的结构,例如运行时常量池、字段与方法信息、方法代码等。JVM 规范把它定义为逻辑区域,不要求采用某一种物理布局或垃圾回收策略。

HotSpot 从 JDK 8 起使用本地内存中的 Metaspace 管理类元数据,替代旧的 Permanent Generation。不能据此把“方法区”和“Metaspace”在所有 JVM 上画等号;前者是规范概念,后者是 HotSpot 实现。

静态字段的 Java 值究竟落在哪个具体物理结构,也不应只靠一张旧版本内存图判断。对应用而言,更稳定的结论是:它属于类状态,通常随定义类和类加载器的生命周期存在。

5. 运行时常量池服务于动态链接

每个类或接口都有一个运行时常量池,由 Class 文件常量池构造。它保存字面量和对类、字段、方法的符号引用,并在解析后关联运行时目标。

运行时常量池是方法区的一部分。字符串驻留池是 String.intern() 对外暴露的另一项机制,不应把“Class 文件常量池”“运行时常量池”和“字符串池”当成同一块表。

6. 本地方法栈服务于 native 调用

JVM 实现可以提供本地方法栈,支持非 Java 语言实现的方法。HotSpot 的具体实现不必与 JVM 栈形成两块可独立观察的内存,但规范上两者职责不同。

JNI 调用还会使用本地堆、句柄和本地库状态。它们会增加进程内存,却不属于 Java 堆。

7. 进程内存不止 JVM 规范中的区域

一个 HotSpot 进程还可能包含:

  • Metaspace 与压缩类空间。
  • JIT 生成代码所在的 Code Cache。
  • GC 数据结构。
  • 平台线程栈。
  • DirectByteBuffer、FFM 和 JNI 分配的本地内存。
  • 内存映射文件与动态链接库。

因此 -Xmx=4g 不代表进程最多使用 4 GB。容器容量要为堆外区域、线程和 JVM 自身开销留出余量。

8. 常见问题

8.1 局部变量都在栈上吗

局部变量的槽位属于栈帧;引用变量保存的是引用值,被引用对象通常由堆管理。JIT 可能把局部值放进寄存器,甚至消除对象,所以“源码变量对应一块固定栈内存”也不是可靠实现结论。

8.2 方法区、永久代和元空间是什么关系

方法区是 JVM 规范定义的逻辑区域。Permanent Generation 是旧 HotSpot 的实现选择,Metaspace 是现代 HotSpot 管理类元数据的实现。后两者不是 Java 语言或 JVM 规范要求。

8.3 直接内存属于 JVM 运行时数据区吗

不属于 JVMS 列出的运行时数据区,但 Java NIO、FFM、JVM 内部和 native 库都可能使用它,它仍计入操作系统看到的进程内存。

9. 面试题

9.1 JVM 有哪些运行时数据区域,哪些在线程间共享

出现公司:阿里巴巴、字节跳动

考察重点

  • 规范区域的职责与共享范围。
  • 栈帧、堆对象和类型数据的生命周期。
  • 方法区与 HotSpot Metaspace 的层次差异。

相关内容:第 1 节“程序计数器记录当前执行位置”至第 7 节“进程内存不止 JVM 规范中的区域”。

参考回答

程序计数器、JVM 栈和本地方法栈按线程存在;堆和方法区由线程共享。每次方法调用产生栈帧,堆保存自动管理的对象,方法区保存类型的运行时结构,每个类型还有运行时常量池。

这是 JVM 规范的逻辑划分。HotSpot 的 Metaspace、Code Cache 和直接内存属于实现与进程容量问题,不能把 -Xmx 当作整个进程的内存上限。

9.2 堆和栈的区别是什么

出现公司:阿里巴巴

考察重点

  • 共享范围、生命周期和管理方式。
  • 引用槽位与对象本身的区别。
  • StackOverflowError 和堆 OOM 的来源。

相关内容:第 2 节“虚拟机栈保存方法调用状态”和第 3 节“堆保存自动管理的对象”。

参考回答

JVM 栈按线程创建,栈帧随方法调用进入和退出,保存局部变量、操作数栈和调用状态;堆由线程共享,保存由 GC 管理的对象和数组。递归使栈深超过实现限制时通常是 StackOverflowError,堆无法为新对象提供空间时可能是 OutOfMemoryError: Java heap space

不要简单说“基本类型在栈、对象在堆”。字段中的基本类型随对象存在于堆,JIT 也可以把值放进寄存器或消除对象。