高频八股 · JVM(3):运行时数据区全景,五块内存分别装什么
先给你一张总表,后面逐块拆:
| 区域 | 线程 | 存什么 | 会 OOM 吗 |
|---|---|---|---|
| 程序计数器 | 私有 | 当前字节码的行号 | 不会 |
| 虚拟机栈 | 私有 | 栈帧(局部变量、操作数栈…) | 会(+ 栈溢出) |
| 本地方法栈 | 私有 | native 方法的栈帧 | 会 |
| 堆 | 共享 | 对象实例、数组 | 会 |
| 方法区 | 共享 | 类元信息、常量、静态变量 | 会 |
记法:私有三块(PC、虚拟机栈、本地方法栈)+ 共享两块(堆、方法区)。
一、程序计数器(PC Register)
它是什么:一块很小的内存,记录当前线程正在执行的字节码行号。
你可以把它理解成"代码执行到哪了"的书签。
为什么它必须是线程私有的?
因为 CPU 是轮流给线程分配时间片的。线程 A 执行到第 10 行被切走,切回来的时候,得知道"我刚才执行到哪了"——这个信息就存在 PC 里。
所以每个线程必须有自己的一份 PC,不能共享。
为什么它不会 OOM?
这是面试最爱问的一句:程序计数器是 JVM 规范里唯一没有规定任何 OutOfMemoryError 情况的区域。
因为它存的东西是定长的(一个行号 / 一个地址),大小固定,不会随程序运行增长。
补充一个细节:执行
native方法时,PC 的值是**未定义(Undefined)**的——因为 native 方法不走字节码,由本地代码执行。
二、虚拟机栈(VM Stack)
生命周期与线程相同:线程创建时创建,线程结束时销毁。
它存的是栈帧(Stack Frame)——每调用一个方法,就压入一个栈帧;方法返回,栈帧弹出。
一个栈帧里有四部分(下一期细讲):
| 组成 | 作用 |
|---|---|
| 局部变量表 | 存方法参数和局部变量 |
| 操作数栈 | 字节码运算的工作区 |
| 动态链接 | 指向运行时常量池中该方法的引用 |
| 方法返回地址 | 方法结束后回到哪里 |
两种异常,别搞混:
| 异常 | 触发条件 |
|---|---|
StackOverflowError | 栈深度超过 -Xss 限制(典型:无限递归) |
OutOfMemoryError | 栈可以动态扩展,但扩展时申请不到内存 |
public void recursion() {
recursion(); // 无限递归 → StackOverflowError
}
注意区分:栈溢出是 StackOverflowError,不是 OOM。这两个是 JVM 里最容易混的一对。
三、本地方法栈(Native Method Stack)
作用跟虚拟机栈一样,只不过它服务的是 native 方法(用 C/C++ 写的方法)。
一个实现细节:在 HotSpot 里,虚拟机栈和本地方法栈是合并实现的——所以你用 jstack 看不到单独的"本地方法栈"。
四、堆(Heap)
JVM 里最大的一块内存,也是 GC 的主战场。
- 线程共享;
- 存对象实例和数组(几乎所有
new出来的东西都在这); - 是 OOM 最常见的发生地。
分代结构(下一期细讲,先有个印象):
堆
├── 新生代(Young)
│ ├── Eden
│ └── Survivor 0 / Survivor 1
└── 老年代(Old / Tenured)
常用参数:
-Xms:堆初始大小;-Xmx:堆最大大小。
生产建议:
-Xms和-Xmx设成一样,避免堆反复扩容带来的抖动。
五、方法区(Method Area)
存什么:
- 类的元信息(类结构、字段、方法、接口);
- 运行时常量池;
- 静态变量;
- JIT 编译后的代码缓存。
注意:方法区 是 JVM 规范里的逻辑概念,不同 JDK 版本的实现不一样。
| 版本 | 实现 | 位置 |
|---|---|---|
| JDK 7 及之前 | 永久代(PermGen) | 堆内(受 -XX:MaxPermSize 限制) |
| JDK 8 及之后 | 元空间(Metaspace) | 本地内存(受 -XX:MaxMetaspaceSize 限制) |
5.1 为什么用元空间替换永久代?
四个原因:
- 永久代大小难调——设小了容易
PermGen spaceOOM,设大了浪费堆; - 永久代在堆里,和对象抢空间——类加载多了会把堆挤爆;
- 永久代的 GC 效率低——回收条件苛刻,容易堆积;
- 和 JRockit 合并——JRockit 没有永久代,为了统一实现。
换成元空间后:类元信息搬到本地内存,理论上只受物理内存限制,
PermGen space这个经典 OOM 也就消失了。
但注意:元空间不是无限大——如果不设上限,类加载过多(比如大量动态代理)同样会 Metaspace OOM。
六、直接内存(Direct Memory)
它不属于运行时数据区,但经常被一起讨论。
它是什么:通过 Unsafe.allocateMemory() 直接向操作系统申请的堆外内存。最常见的使用者是 NIO 的 DirectByteBuffer。
ByteBuffer buf = ByteBuffer.allocateDirect(1024 * 1024); // 1MB 直接内存
为什么要用它?
因为少一次拷贝。
普通堆内 buffer 做 IO 时,数据要从**堆内 → 堆外(内核缓冲区)**拷一次;直接内存本来就在堆外,省掉了这一步,所以 NIO 高吞吐场景更愿意用它。
代价也很明显:
| 优点 | 缺点 |
|---|---|
| 少一次内存拷贝,IO 更快 | 分配和回收成本高(不受 GC 直接管理) |
不受 -Xmx 限制 | 受物理内存限制,容易把机器吃满 |
| 适合长生命周期的缓冲区 | 出问题难排查 |
参数:-XX:MaxDirectMemorySize(不设时默认约等于 -Xmx)。
它也会 OOM,报错是:
java.lang.OutOfMemoryError: Direct buffer memory
七、全景对比:每个区域怎么"爆"
| 区域 | 关键参数 | 典型 OOM 场景 |
|---|---|---|
| 程序计数器 | — | 不会 OOM |
| 虚拟机栈 | -Xss | 递归太深(StackOverflowError)/ 线程太多 |
| 本地方法栈 | -Xss | 同上 |
| 堆 | -Xms / -Xmx | 对象太多、内存泄漏 |
| 方法区(元空间) | -XX:MaxMetaspaceSize | 类加载过多(动态代理、热部署) |
| 直接内存 | -XX:MaxDirectMemorySize | NIO 大缓冲区、未释放 |
一个实用的排查思路:看到 OOM,先看报错的后缀——
Java heap space是堆,Metaspace是元空间,Direct buffer memory是直接内存,unable to create native thread是线程数到顶。
八、高频面试题速答
Q1:JVM 运行时数据区有哪几块?
程序计数器、虚拟机栈、本地方法栈(私有),堆、方法区(共享)。
Q2:为什么程序计数器不会 OOM?
它存的是定长的字节码行号,大小固定,不会随运行增长;JVM 规范也没规定它会抛 OOM。
Q3:StackOverflowError 和 OutOfMemoryError 有什么区别?
前者是栈深度超限(如无限递归),后者是申请不到内存(栈扩展失败、堆满、元空间满等)。
Q4:JDK 8 为什么用元空间替换永久代?
永久代大小难调、和堆抢空间、GC 效率低,且与 JRockit 合并需要统一实现。元空间放在本地内存,更灵活。
Q5:直接内存是 JVM 管理的吗?
不是。它由操作系统分配,通过 DirectByteBuffer 使用,不受 -Xmx 限制,但受 -XX:MaxDirectMemorySize 和物理内存限制。
Q6:堆和栈的区别?
堆线程共享、存对象、GC 管理、空间大;栈线程私有、存栈帧、随方法调用自动出入、空间小但快。
九、一句话总结
私有三块(PC、虚拟机栈、本地方法栈)管"执行",共享两块(堆、方法区)管"存放";PC 是唯一不 OOM 的,堆是最容易爆的,直接内存是"不归 JVM 管但会拖垮机器"的。
(本系列持续更新中,共 10 期,敬请关注~)
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2401_88601046/article/details/169267062




