★ JVM 内存区域如何划分?各自存放什么、哪些会抛 OOM?
结论先行:JVM 运行时内存按“是否线程共享”分为两类。线程私有区——程序计数器、虚拟机栈、本地方法栈,随线程创建和销毁;线程共享区——堆、方法区,随 JVM 启停。除程序计数器外其余区域都可能抛 OOM,而虚拟机栈深度超限会先抛 StackOverflowError。
核心区域速览
| 区域 | 线程共享 | 存放内容 | 异常表现 |
|---|---|---|---|
| 程序计数器 | 否 | 当前线程执行到的字节码行号,供分支/循环/异常/线程切换后恢复 | 无(唯一不抛 OOM) |
| 虚拟机栈 | 否 | 栈帧:局部变量表、操作数栈、动态链接、返回地址 | 深度不足抛 StackOverflowError,扩展失败抛 OOM |
| 本地方法栈 | 否 | 为 native 方法调用服务 | 同虚拟机栈 |
| 堆 | 是 | 几乎全部对象实例与数组,GC 主战场 | OOM:Java heap space |
| 方法区 | 是 | 类型信息、运行时常量池、静态变量、JIT 编译缓存 | OOM:Metaspace |
版本演进要点
JDK 8 起方法区的落地实现改为元空间(Metaspace),直接使用本地内存,默认大小不设上限,避免永久代大小难以预估导致 Full GC;字符串常量池则从方法区移入堆中。对象引用(局部变量)存在栈上,对象本体在堆上。
// 堆 OOM 现场:用 -Xmx 限制堆上限,持续分配即可复现
public class HeapOomDemo {
public static void main(String[] args) {
java.util.List<byte[]> list = new java.util.ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024]); // 每次分配 1MB 且被引用
}
}
}
// 栈溢出现场:无限递归撑爆单线程调用栈
public class StackOverflowDemo {
public static void main(String[] args) {
recurse(1);
}
static int recurse(int n) {
return recurse(n + 1);
}
}
常见追问 / 记忆点
- 记忆口诀:三个私有(计数器、虚拟机栈、本地方法栈)+ 两个共享(堆、方法区),只有程序计数器不会 OOM。
- 追问:JDK 8 用元空间替换永久代,顺带把字符串常量池留在堆里,减少元空间扩容引发的 Full GC。
- 追问:栈帧里局部变量表存的是引用与基本类型,真正对象数据在堆上;-Xss 控制栈深、-Xmx 控制堆上限。