2026年7月11日 · 5 分钟阅读
JVM 运行时数据区:堆、栈、方法区、程序计数器与直接内存
梳理 JVM 运行时数据区,包括堆、新生代、老年代、虚拟机栈、本地方法栈、方法区、元空间、字符串常量池和直接内存。
JVM 运行 Java 程序时,会把内存划分成多个区域。理解这些区域,是分析对象分配、栈溢出、内存溢出、GC 行为和性能问题的基础。
运行时数据区可以先按线程是否共享来理解。
| 区域 | 是否线程共享 | 主要作用 |
|---|---|---|
| 堆 | 共享 | 存放对象实例和数组 |
| 方法区 | 共享 | 存放类元信息、常量、静态变量等 |
| 虚拟机栈 | 线程私有 | 存放方法调用栈帧 |
| 本地方法栈 | 线程私有 | 支持 Native 方法调用 |
| 程序计数器 | 线程私有 | 记录当前线程执行位置 |
| 直接内存 | 共享但不属于 JVM 运行时数据区 | NIO 等场景使用的堆外内存 |
堆
堆是 JVM 中最大的一块内存,也是 GC 管理的核心区域。绝大多数对象实例和数组都分配在堆中。
堆通常按对象生命周期分代:
堆 = 新生代 + 老年代
新生代 = Eden + Survivor0 + Survivor1
大部分对象朝生夕死,适合先放在新生代;存活时间较长的对象会逐步晋升到老年代。这就是分代收集的基础。
为什么需要两个 Survivor
新生代一般由 Eden 和两个 Survivor 区组成。对象先分配在 Eden,Minor GC 后仍然存活的对象会进入一个 Survivor。
两个 Survivor 的意义在于复制整理:
- Eden 和一个 Survivor 中的存活对象复制到另一个空 Survivor。
- 原 Eden 和使用过的 Survivor 被清空。
- 两个 Survivor 角色互换。
这样可以减少内存碎片,并让新生代回收更高效。
老年代
老年代主要存放生命周期较长的对象。对象进入老年代的常见情况包括:
- 年龄达到晋升阈值。
- 大对象直接进入老年代。
- Survivor 空间不足,触发空间担保。
- 动态年龄判断后提前晋升。
老年代回收成本更高。如果老年代持续增长,通常要关注对象是否被长期引用、本地缓存是否过大、是否存在内存泄漏。
虚拟机栈
每个 Java 线程都有自己的虚拟机栈。线程执行一个方法时,会创建一个栈帧;方法执行结束,对应栈帧出栈。
栈帧里通常包含:
- 局部变量表。
- 操作数栈。
- 动态链接。
- 方法返回地址。
递归过深或栈空间设置过小,可能导致 StackOverflowError。线程数量过多,也会消耗大量栈内存。
本地方法栈
本地方法栈服务于 Native 方法。Java 有些底层能力依赖 C/C++ 实现,比如线程创建、文件 IO、网络 IO 等。
本地方法栈和虚拟机栈职责类似,只是服务对象不同:虚拟机栈服务 Java 方法,本地方法栈服务 Native 方法。
方法区与元空间
方法区是 JVM 规范中的概念,用于存储类相关信息。HotSpot 在 JDK 1.8 之前用永久代实现方法区,JDK 1.8 之后改为元空间。
方法区里常见内容包括:
- 类信息。
- 方法信息。
- 字段信息。
- 运行时常量池。
- JIT 编译后的代码缓存等。
永久代和元空间的重要区别是:永久代在 JVM 堆内或与堆强相关,元空间使用本地内存。
为什么用元空间替代永久代
永久代容易遇到几个问题:
- 类元信息大小不容易预估。
- 字符串常量池曾经放在永久代里,容易造成永久代溢出。
- 永久代 GC 复杂度高,回收效率也不理想。
- HotSpot 后续需要与其他虚拟机实现融合。
元空间使用本地内存,减少了永久代大小固定带来的限制。不过元空间也不是无限的,类加载过多仍然可能导致元空间 OOM。
字符串常量池
字符串常量池用于复用字符串对象,减少重复字符串带来的内存浪费。
需要区分几个概念:
- Class 文件常量池:存在于 class 文件中。
- 运行时常量池:类加载后进入方法区。
- 字符串常量池:用于字符串对象复用,JDK 1.7 后移动到堆中。
String.intern() 和字符串字面量相关问题,很多都绕不开字符串常量池。
程序计数器
程序计数器是线程私有的小块内存,用来记录当前线程下一条要执行的字节码指令地址。
为什么需要它?因为线程会发生上下文切换。CPU 从线程 A 切到线程 B,再切回线程 A 时,需要知道线程 A 上次执行到哪里。
程序计数器就是这个“执行位置”的记录器。
直接内存
直接内存不是 JVM 运行时数据区的一部分,但在 Java 程序中非常常见,尤其是 NIO、Netty、文件传输等场景。
直接内存的优势是可以减少堆内和堆外之间的数据复制,提高 IO 性能。但它也带来一个风险:不受堆大小直接限制,如果使用不当,可能出现堆看起来不大、进程内存却很高的情况。
常见问题怎么定位
| 现象 | 优先关注区域 |
|---|---|
OutOfMemoryError: Java heap space | 堆 |
StackOverflowError | 虚拟机栈 |
| 线程很多,内存上涨 | 线程栈 |
| 类加载过多,元空间上涨 | 元空间 |
| 进程内存高于堆很多 | 直接内存 |
| Full GC 频繁 | 老年代、元空间、大对象 |
小结
JVM 内存区域可以这样记:
- 堆负责对象。
- 栈负责方法调用。
- 方法区负责类信息。
- 程序计数器负责线程执行位置。
- 本地方法栈负责 Native 调用。
- 直接内存负责部分高性能 IO 场景。
后续分析对象创建、GC 和 JVM 调优时,所有问题都会回到这些区域上。