JVM 运行时内存区域如何划分的?
结论先行:按线程归属划分最清晰:线程私有有程序计数器、虚拟机栈、本地方法栈,随线程生死;线程共享有堆、方法区(JDK 8+ 落地为元空间)。此外还有规范之外但真实存在的直接内存(direct memory)与代码缓存(CodeCache)。除程序计数器外,每个区域都有对应的 OOM 消息,答题时把「区域 → 存什么 → 对应参数 → 异常消息」串成一条线最稳。
一、区域总表
| 区域 | 线程共享 | 存放 | 关键参数 | 异常 |
|---|---|---|---|---|
| 程序计数器 | 否 | 当前字节码行号 | 无 | 无 |
| 虚拟机栈 | 否 | 栈帧:局部变量表、操作数栈、动态链接、返回地址 | -Xss | StackOverflowError / OOM |
| 本地方法栈 | 否 | native 方法调用帧 | -Xss | 同上 |
| 堆 | 是 | 对象实例、数组、字符串池(JDK 7+) | -Xms、-Xmx | Java heap space |
| 方法区 / 元空间 | 是 | 类元数据、运行时常量池、静态变量 | -XX:MetaspaceSize、-XX:MaxMetaspaceSize | Metaspace |
| 直接内存 | 是 | ByteBuffer.allocateDirect、Netty ByteBuf | -XX:MaxDirectMemorySize | Direct buffer memory |
| 代码缓存 | 是 | JIT 编译后的机器码 | -XX:ReservedCodeCacheSize | 编译线程停止,性能下降 |
二、栈帧结构
虚拟机栈(每个线程一份)
├── 栈帧(每次方法调用压入一个)
│ ├── 局部变量表:this、参数、局部变量(槽位 4 字节,long/double 占 2 槽)
│ ├── 操作数栈:字节码运算的临时工作区
│ ├── 动态链接:指向运行时常量池中该方法的方法引用
│ └── 返回地址:方法正常/异常退出后回到哪
└── 栈深度上限由 -Xss 决定
三、堆的分代与分配
堆(-Xmx)
├── 新生代(Young)
│ ├── Eden(-XX:SurvivorRatio 决定占比)
│ ├── Survivor S0
│ └── Survivor S1
└── 老年代(Old)
对象分配优先走 TLAB(线程本地分配缓冲),避免多线程争用 Eden 指针
public class RegionDemo {
public static void main(String[] args) {
// 堆:对象实例
byte[] onHeap = new byte[8 * 1024 * 1024];
// 直接内存:不受 -Xmx 约束,受 -XX:MaxDirectMemorySize 与物理内存约束
java.nio.ByteBuffer direct = java.nio.ByteBuffer.allocateDirect(8 * 1024 * 1024);
System.out.println("heap=" + onHeap.length + " direct=" + direct.capacity());
}
}
# 分别把两个区域打爆,观察不同的 OOM 消息
java -Xmx32m -cp . RegionDemo # OutOfMemoryError: Java heap space
java -Xmx512m -XX:MaxDirectMemorySize=16m -cp . RegionDemo # OutOfMemoryError: Direct buffer memory
java -Xss256k -cp . RecursionDemo # StackOverflowError
四、规范与实现的差异
| 概念 | JVM 规范 | HotSpot 实现 |
|---|---|---|
| 方法区 | 逻辑区域,规范未规定实现方式 | JDK 7 永久代 → JDK 8 元空间(本地内存) |
| 运行时常量池 | 属于方法区 | 元空间内,字符串字面量落堆 |
| 字符串常量池 | 未单独定义 | JDK 7 起从永久代移入堆 |
| 栈 | 未规定是否合并 | 虚拟机栈与本地方法栈合并实现 |
这就是为什么「方法区 OOM」在 JDK 8 之后变成「Metaspace OOM」——永久代被移除,元空间改用本地内存。
五、常见追问
| 追问 | 回答要点 |
|---|---|
| 为什么程序计数器不会 OOM | 它只存一个行号偏移,大小固定,且是唯一没有规定 OOM 的区域 |
| 栈溢出和堆溢出怎么区分 | 栈溢出是 StackOverflowError(Error 子类,非 OOM),堆溢出是 OutOfMemoryError: Java heap space |
| 直接内存算 JVM 内存吗 | 不算堆,但算进进程 RSS;容器里最容易被忽略,是 OOMKilled 的常见原因 |
| 元空间为什么不设上限 | 默认无上限,类多时按需增长;生产建议设 MaxMetaspaceSize 兜底 |
| 对象一定分配在堆上吗 | 不一定,逃逸分析后可能栈上分配或标量替换,但这是 JIT 优化,不是语言语义 |
小结
七个区域、两条线程归属线、四类异常消息,构成内存划分的完整答案。把「直接内存」和「代码缓存」补上,答案就从课本级别升到能解释线上问题的级别。