第2章 JVM内存模型详解:堆、栈、方法区、本地方法栈、程序计数器
说内存模型之前,咱得先搞清楚一件事:JVM 到底把咱们写的那点代码、那些对象、那些方法调用,都塞到了哪几个“格子”里。你要是连这个都不门儿清,后面分析 Heap Dump 就是瞎蒙。我个人习惯把 JVM 内存模型看作五个兄弟——堆、虚拟机栈、方法区、本地方法栈、程序计数器。它们各管一摊,有的共享,有的私有,谁出了毛病都会引发连锁反应。
2.1 堆(Heap)——OOM 的头号嫌疑人
堆,说白了就是 Java 对象的老家。所有 new 出来的东西,只要不是特别小的非逃逸对象,都搁这儿待着。它是所有线程共享的,也是 OOM 发生概率最高的区域。我记得有一次线上服务连续告警,一查 Heap Dump,发现一个 HashMap 里塞了几百万个对象,key 还是那种不会重复的业务标识——典型的无界缓存坑了系统。
堆内部又分几块,大部分收集器都遵循“年轻代 + 老年代”的划分。咱简单列个表,面试常考:
| 区域 | 作用 | GC 频率 | OOM 可能性 |
|---|---|---|---|
| Eden区 | 大部分对象出生地 | 极高,Minor GC 频繁 | 中等 |
| Survivor区(From/To) | 存放年轻代幸存对象 | 每次 Minor GC 后交换 | 低 |
| 老年代 | 长期存活的对象(如缓存、单例) | 相对较低,Full GC 时触发 | 高 |
2.2 虚拟机栈(JVM Stack)——方法调用的「记账本」
每个线程都背着一个小本本,就是虚拟机栈。你每调用一个方法,栈上就压入一个栈帧(Stack Frame),里面装着局部变量表、操作数栈、动态链接、方法出口等。方法执行完,栈帧弹出,干干净净。
这里最容易出的事故是 StackOverflowError——递归太深或者无限循环调用。我遇到过一个同事写的 JSON 解析,碰到循环引用就死循环递归,一天报警几十次。一般默认栈大小 1024KB,如果确定需要更深调用,可以用 -Xss 调大,但别太夸张,否则线程数上不去。
-Xss256k 或 -Xss512k 多半够用。你想想看,一个后台服务几百个线程,每个省 512KB 就省很多内存了。
2.3 本地方法栈(Native Method Stack)
这个区域专为 native 方法服务。比如你用 Java 调用了 C/C++ 写的 so 库(像 netty 里的 native epoll),里面的栈信息就记在这里。它和虚拟机栈很像,只不过管的是 native 代码。我记得有一次排查一个周期性内存泄漏,Heap Dump 看不出毛病,后来用 pmap 看进程内存,才发现 native 侧分配了巨量虚拟内存,最后定位到一个第三方加密库没释放资源。
2.4 程序计数器(Program Counter Register)
很小的一块内存,每个线程独一份。它存的是当前线程正在执行的字节码指令地址。说白了,就是告诉 JVM:“下一条该执行哪条指令了”。
这东西在 Java 虚拟机规范里是唯一不会出现 OOM 的区域。因为它的变量就那么一点大,JVM 实现基本用寄存器或者固定大小的内存来模拟。你可能觉得它不重要,但在多线程切换时,程序计数器是恢复执行现场的命根子。我曾经在调试一个诡异线程挂起问题时,用 jstack 看了线程栈,再对照程序计数器的位置,才找出死锁的循环点。
2.5 方法区(Method Area)——类的“档案室”
方法区放的是类的结构信息:类名、字段描述、方法字节码、常量池、静态变量、JIT 编译后的代码等等。在 JDK 8 以前它叫“永久代”(PermGen),JDK 8 以后换成了“元空间”(Metaspace),并且从堆里搬到了本地内存。
为什么这么改?因为永久代的大小很难预估,老容易 OOM。我记得在 JDK 7 时代,一个大型应用挂掉,Heap Dump 一看,永久代被动态生成的 CGLIB 代理类撑爆了。换成元空间后,默认只用本地内存限制,但也要注意设置 -XX:MaxMetaspaceSize,否则虚拟内存使用失控。
| 对比项 | 永久代 (JDK ≤ 7) | 元空间 (JDK ≥ 8) |
|---|---|---|
| 位置 | 堆内 | 本地内存(不在堆内) |
| 默认大小 | 受 -XX:PermSize / -XX:MaxPermSize 控制 | 无上限(取决于物理内存),可设置 -XX:MetaspaceSize |
| 常见 OOM | java.lang.OutOfMemoryError: PermGen space | java.lang.OutOfMemoryError: Metaspace(一般不会) |
好了,五个区域都过了一遍。你捋一捋:共享区(堆 + 方法区)里面存的是对象和类元信息,私有区(三个小区域)管的是执行上下文和线程控制。记住这个分层,后面分析 Heap Dump 时你就能一眼看出问题出在哪个“格子”里了。