JVM 运行时数据区
2025/12/20大约 7 分钟约 2022 字
基于 JDK 8+ 的 JVM 运行时数据区深度解析。
一、全景概览
1.1 运行时数据区架构

一句话总结:堆管存储,栈管运行。
- 堆(Heap):存放对象实例,是 GC 的主战场
- 栈(Stack):管理方法调用和局部变量,LIFO 结构
1.2 线程私有 vs 线程共享
| 类型 | 区域 | 生命周期 |
|---|---|---|
| 线程私有 | 程序计数器、虚拟机栈、本地方法栈 | 随线程生灭 |
| 线程共享 | 堆、方法区(元空间) | 随 JVM 生灭 |
二、线程私有区域
2.1 程序计数器(PC Register)
定义:当前线程所执行的字节码的行号指示器。
执行 Java 方法时:PC = 当前字节码指令地址
执行 Native 方法时:PC = 空(Undefined)作用:
- 字节码解释器通过改变 PC 来选取下一条要执行的指令
- 多线程切换后,通过 PC 恢复到正确的执行位置
【重点】唯一不会 OOM 的区域
原因:PC 只存储一个固定大小的内存地址(或空),不会动态扩展。
2.2 虚拟机栈(Java VM Stack)
定义:每个线程在创建时都会创建一个虚拟机栈,内部保存多个栈帧(Stack Frame)。

栈帧的四个组成部分
| 组件 | 作用 | 通俗比喻 |
|---|---|---|
| 局部变量表 | 存放方法参数和局部变量(基本类型值 + 对象引用) | 方法的"私人储物柜" |
| 操作数栈 | 计算过程中的临时数据存储,LIFO | 方法的"计算草稿纸" |
| 动态链接 | 指向运行时常量池中该方法的符号引用 | 方法的"名片",用于找到真正的方法入口 |
| 方法返回地址 | 方法正常退出或异常退出后,返回到调用者的位置 | 方法的"回家路线" |
局部变量表详解
┌─────────────────────────────────────────────────────┐
│ 局部变量表 (Slot 数组) │
├──────┬──────┬──────┬──────┬──────┬──────┬──────────┤
│ this │ arg1 │ arg2 │ var1 │ var2 │ ... │ (实例方法) │
├──────┴──────┴──────┴──────┴──────┴──────┴──────────┤
│ arg1 │ arg2 │ var1 │ var2 │ ... │ │ (静态方法) │
└─────────────────────────────────────────────────────┘- 每个 Slot 占用 32 位(4 字节)
- long 和 double 占用 2 个 Slot
- Slot 可复用:离开作用域的变量其 Slot 可被后续变量使用
操作数栈工作原理
int a = 1;
int b = 2;
int c = a + b;字节码:
iconst_1 // 将 1 压入操作数栈
istore_1 // 弹出栈顶,存入局部变量表 slot1 (a)
iconst_2 // 将 2 压入操作数栈
istore_2 // 弹出栈顶,存入局部变量表 slot2 (b)
iload_1 // 将 slot1 (a=1) 压入操作数栈
iload_2 // 将 slot2 (b=2) 压入操作数栈
iadd // 弹出两个操作数,相加,结果压栈
istore_3 // 弹出栈顶,存入局部变量表 slot3 (c)异常分析
| 异常 | 触发场景 | 示例 |
|---|---|---|
| StackOverflowError | 栈深度超过允许的最大深度 | 无限递归 |
| OutOfMemoryError | 栈扩展时无法申请到足够内存 | 创建大量线程 |
// StackOverflowError 示例
public void recursive() {
recursive(); // 无限递归
}
// 调整栈大小:-Xss256k(默认 1MB)2.3 本地方法栈(Native Method Stack)
定义:为 Native 方法(如 C/C++ 实现)服务的栈。
public native int hashCode(); // Object.hashCode() 是 native 方法与虚拟机栈的关系:
- HotSpot VM 将虚拟机栈与本地方法栈合二为一
- 同样可能抛出 StackOverflowError 和 OutOfMemoryError
JNI 交互:Java 通过 JNI(Java Native Interface)调用 C/C++ 代码,本地方法栈负责管理这些调用的上下文。
三、线程共享区域
3.1 堆(Java Heap)
定义:JVM 管理的最大内存区域,用于存放对象实例和数组。

内存划分(分代模型)
┌─────────────────────────────────────────────────────────────┐
│ Java Heap │
├─────────────────────────────────┬───────────────────────────┤
│ Young Generation │ Old Generation │
│ (新生代) │ (老年代) │
├──────────┬──────────┬───────────┤ │
│ Eden │ S0 │ S1 │ │
│ (伊甸区) │ (幸存区0) │ (幸存区1) │ │
│ 8/10 │ 1/10 │ 1/10 │ 2/3 堆 │
└──────────┴──────────┴───────────┴───────────────────────────┘
1/3 堆默认比例:
- 新生代 : 老年代 = 1 : 2(
-XX:NewRatio=2) - Eden : S0 : S1 = 8 : 1 : 1(
-XX:SurvivorRatio=8)
对象分配策略
- 优先在 Eden 分配
- 大对象直接进入老年代(
-XX:PretenureSizeThreshold) - 长期存活的对象进入老年代(年龄阈值默认 15,
-XX:MaxTenuringThreshold) - 动态年龄判断:Survivor 中相同年龄对象总大小 > Survivor 空间一半,则 >= 该年龄的对象直接进入老年代
TLAB(Thread Local Allocation Buffer)
问题:堆是线程共享的,多线程并发分配对象需要加锁,效率低下。
解决方案:TLAB —— 在 Eden 区为每个线程预分配一小块私有内存。
┌─────────────────────────────────────────────────────────────┐
│ Eden │
├───────────┬───────────┬───────────┬─────────────────────────┤
│ TLAB-A │ TLAB-B │ TLAB-C │ 剩余 Eden 空间 │
│ (线程A私有)│ (线程B私有)│ (线程C私有)│ │
└───────────┴───────────┴───────────┴─────────────────────────┘分配流程:
- 线程在自己的 TLAB 中分配(无锁,指针碰撞)
- TLAB 用完 → 申请新的 TLAB 或直接在 Eden 分配(需要 CAS)
- 对象太大无法放入 TLAB → 直接在 Eden 或老年代分配
相关参数:
-XX:+UseTLAB:启用 TLAB(默认开启)-XX:TLABSize:设置 TLAB 大小
异常:Heap OOM
// 常见原因
1. 内存泄漏:对象持续创建但未释放
2. 堆设置过小:-Xmx 设置不合理
3. 大对象/大数组:一次分配超过可用内存
// 报错信息
java.lang.OutOfMemoryError: Java heap space
// 排查参数
-XX:+HeapDumpOnOutOfMemoryError // OOM 时生成堆转储
-XX:HeapDumpPath=/path/to/dump3.2 方法区 / 元空间(Method Area / Metaspace)
定义:存储已加载的类信息、常量、静态变量、JIT 编译后的代码等。
演进史:PermGen → Metaspace
| 版本 | 实现 | 存储位置 | OOM 信息 |
|---|---|---|---|
| JDK 7 及之前 | 永久代(PermGen) | JVM 堆内存 | PermGen space |
| JDK 8 及之后 | 元空间(Metaspace) | 本地内存(Native Memory) | Metaspace |
为什么用本地内存?
- PermGen 大小固定,难以调优
- 类卸载困难,容易 OOM
- 本地内存更大,由 OS 管理
相关参数:
-XX:MetaspaceSize=256m // 初始大小(触发 Full GC 的阈值)
-XX:MaxMetaspaceSize=512m // 最大大小(默认无限制)运行时常量池(Runtime Constant Pool)
定义:Class 文件中的常量池表(Constant Pool Table)在运行时的表示形式。
存储内容:
- 字面量:字符串常量、final 常量值
- 符号引用:类和接口的全限定名、字段名、方法名
解析过程:
编译期:符号引用(类名字符串如 "java/lang/Object")
↓ 类加载 - 解析阶段
运行期:直接引用(内存地址/偏移量)字符串常量池(StringTable)
JDK 6:在永久代中 JDK 7+:移动到堆中
原因:
- 永久代 GC 效率低,字符串驻留过多导致 PermGen OOM
- 移到堆中可以享受更高效的 GC
// intern() 方法
String s = new String("abc");
String interned = s.intern(); // 放入/查找 StringTable四、堆外内存(Direct Memory)
定义:不属于 JVM 运行时数据区,但被 NIO 频繁使用的本地内存。
NIO 零拷贝原理
传统 IO(两次拷贝):
磁盘 → 内核缓冲区 → JVM 堆 → 内核缓冲区 → 网卡
NIO Direct Memory(一次拷贝):
磁盘 → 内核缓冲区 → Direct Memory ←→ 网卡
↑
JVM 直接读写这块内存创建方式:
ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024); // 1MB回收机制
Direct Memory 不受 GC 直接管理,通过以下方式回收:
Cleaner(虚引用 + ReferenceQueue)
- DirectByteBuffer 关联一个 Cleaner 对象
- 当 DirectByteBuffer 被 GC 回收时,Cleaner 被放入 ReferenceQueue
- Reference Handler 线程调用 Cleaner.clean() 释放本地内存
手动释放(Unsafe)
((DirectBuffer) buffer).cleaner().clean(); // 不推荐
异常
// OOM 错误
java.lang.OutOfMemoryError: Direct buffer memory
// 调整参数
-XX:MaxDirectMemorySize=256m // 默认与 -Xmx 相同五、总结与对比
| 区域 | 生命周期 | 作用 | 线程共享 | 可能异常 |
|---|---|---|---|---|
| 程序计数器 | 随线程 | 记录当前执行的字节码地址 | 私有 | 无 |
| 虚拟机栈 | 随线程 | 管理方法调用(栈帧) | 私有 | SOF / OOM |
| 本地方法栈 | 随线程 | 管理 Native 方法调用 | 私有 | SOF / OOM |
| 堆 | 随 JVM | 存放对象实例 | 共享 | OOM |
| 方法区/元空间 | 随 JVM | 存放类信息、常量、静态变量 | 共享 | OOM |
| 直接内存 | 手动管理 | NIO 缓冲区 | 共享 | OOM |
