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 的意义在于复制整理:

  1. Eden 和一个 Survivor 中的存活对象复制到另一个空 Survivor。
  2. 原 Eden 和使用过的 Survivor 被清空。
  3. 两个 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 调优时,所有问题都会回到这些区域上。