JVM 内存与 OOM 排查深度话题
内存区域 · GC(G1/CMS) · OOM 类型 · jmap/jstack/MAT 排查 · GC 调优
🎬 怎么引出
「JVM 我处理过线上 OOM。一次是堆 OOM — 报表导出一次性查了 50 万条数据全放内存,堆撑爆。我用 jmap dump 堆快照,MAT 打开看支配树(Dominator Tree),定位到一个 ArrayList 占了 80% 内存,改成流式处理解决。还有一次是 Metaspace OOM — 动态生成的代理类太多没释放。」JVM + 真实排查故事,高级岗位加分项。
1
JVM 内存区域
| 区域 | 存什么 | 线程共享 | OOM 类型 |
|---|---|---|---|
| 堆(Heap) | 对象实例、数组 | 共享 | java.lang.OutOfMemoryError: Java heap space |
| 方法区/元空间(Metaspace) | 类信息、常量池、静态变量 | 共享 | Metaspace(JDK8+) |
| 虚拟机栈 | 栈帧(局部变量、操作数栈) | 私有 | StackOverflowError |
| 本地方法栈 | Native 方法 | 私有 | StackOverflowError |
| 程序计数器 | 当前指令地址 | 私有 | 不会 OOM |
| 直接内存 | NIO Buffer 等 | - | Direct buffer memory |
JDK 版本区别: JDK7 前永久代(PermGen,堆的一部分);JDK8+元空间(Metaspace,用本地内存,不在堆里)。这是高频考点,很多老资料还讲 PermGen。
2
GC 垃圾回收
🧠 分代收集
堆分新生代(Young)和老年代(Old)。新生代又分 Eden + 2 个 Survivor(S0/S1)。对象先在 Eden 分配 → Minor GC 存活的进 Survivor → 经过多次 GC 还活着进老年代。
Minor GC(新生代,频繁快) / Major GC(老年代) / Full GC(全堆,慢,STW)。
📋 GC 算法
- 标记-清除: 标记存活,清除其余。产生碎片
- 标记-复制: 存活对象复制到另一半。新生代用(Survivor)
- 标记-整理: 标记后整理到一端。老年代用
- 分代收集: 新生代用复制,老年代用整理
📋 垃圾收集器
| 收集器 | 特点 | 适用 |
|---|---|---|
| Serial | 单线程,STW | 客户端/小应用 |
| Parallel(默认 JDK8) | 多线程,吞吐量优先 | JDK8 默认 |
| CMS | 并发,低停顿(已废弃 JDK9+) | 对延迟敏感 |
| G1(默认 JDK9+) | 分 Region,可预测停顿 | 大堆,现代首选 |
| ZGC/Shenandoah | 超低停顿(<10ms) | 超大堆,JDK11+ |
判断对象死亡: ① 引用计数法(循环引用问题,Java 不用);② 可达性分析(GC Roots 到对象有路径就活着)。四种引用: 强(永不回收)→ 软(内存不足回收)→ 弱(下次 GC 回收)→ 虚(随时回收)。
🔑 GC Roots 有哪些(面试背全)
| GC Root | 具体是什么 | 泄漏概率 | 泄漏场景 |
|---|---|---|---|
| ① 栈局部变量 | 方法里正在用的对象 | 极低 | 方法结束自动出栈断开 |
| ② 静态变量 | static Map cache | 极高 | 只 put 不 remove → 无限增长 → OOM |
| ③ 活跃线程 | 线程本身是 Root | 极高 | ThreadLocal 没 remove + 线程池复用 |
| ④ 常量引用 | static final | 低 | 较少见 |
| ⑤ 同步锁 | synchronized 持有的对象 | 低 | 锁没释放 → 关联对象不回收 |
| ⑥ JNI 引用 | native 代码持有的 Java 对象 | 低 | native 忘释放 |
🤔 普通变量会泄漏吗? 会,但很容易被 GC 自动回收。局部变量方法结束自动断开 → 安全。static/ThreadLocal 永久连着 Root → 必须手动 remove。核心规律:变量本身不泄漏,泄漏的是"引用链连着 GC Root 断不掉"。
3
OOM 类型
| OOM 信息 | 原因 | 排查 |
|---|---|---|
| Java heap space | 堆内存不够 / 内存泄漏 | jmap + MAT |
| GC overhead limit | GC 花太多时间回收太少 | 堆泄漏前兆 |
| Metaspace | 类元数据太多(动态代理) | -XX:MaxMetaspaceSize |
| Direct buffer memory | NIO 直接内存不够 | -XX:MaxDirectMemorySize |
| unable to create new native thread | 线程数太多(系统级) | 查线程泄漏 |
| StackOverflowError | 栈溢出(死递归) | 看堆栈深度 |
4
OOM 排查工具
📋 排查流程
# 1. 找到占用高的 Java 进程
jps -lvm
top -c # 找 Java 进程 PID
# 2. 看堆内存概况
jmap -heap <PID>
# 3. 看对象统计(哪些对象占得多)
jmap -histo:live <PID> | head -20
# 4. dump 堆快照(生产慎用,会 STW)
jmap -dump:format=b,file=heap.hprof <PID>
# 5. MAT 打开 heap.hprof
# - Dominator Tree(支配树):谁占了最多内存
# - Leak Suspects(泄漏嫌疑):自动分析
# - 找到 GC Root 引用链
# 线程问题用 jstack
jstack <PID> > thread.txt # dump 线程,看死锁/阻塞
⚠️ 生产 dump 的坑
jmap -dump 会触发 Full GC(STW,暂停所有应用)。生产环境直接 dump 可能导致服务卡顿几十秒。正确做法: ① 提前配 -XX:+HeapDumpOnOutOfMemoryError(OOM 时自动 dump);② 用阿里 arthas 的 heapdump(影响小);③ 从节点 dump,不在主节点。
5
GC 调优
常见参数:
-Xms 初始堆, -Xmx 最大堆(建议 Xms=Xmx 避免动态扩容), -Xmn 新生代, -XX:MetaspaceSize 元空间, -XX:+UseG1GC 用 G1, -XX:MaxGCPauseMillis=200 目标停顿时间。原则:Xms=Xmx,生产用 G1,别过度调优。
调优时机: 不要为调优而调优。 只在:① 频繁 Full GC;② GC 停顿影响响应;③ OOM 频发。否则默认参数(JDK9+ G1)已经很合理。先看 GC 日志(
-Xlog:gc*)找问题。
6
高频追问
1JVM 内存区域有哪些? 必问
堆(对象)、方法区/元空间(JDK8+ 元空间,类信息)、虚拟机栈(栈帧)、本地方法栈、程序计数器、直接内存。堆和元空间共享,栈和计数器线程私有。重点:JDK8 后 PermGen 改 Metaspace(本地内存)。
2怎么判断对象可以被回收? 高频
可达性分析:从 GC Roots 出发,有路径可达就活着,不可达就回收。GC Roots:栈局部变量、静态变量、JNI 引用、活动线程。引用计数有循环引用问题,Java 不用。四种引用:强→软→弱→虚。
3线上 OOM 怎么排查? 高频
① 提前配
-XX:+HeapDumpOnOutOfMemoryError 自动 dump;② 或用 jmap -dump(注意 STW);③ MAT 打开 hprof:看 Dominator Tree 找大对象,Leak Suspects 自动分析,找 GC Root 引用链;④ 定位是泄漏(对象一直不释放)还是溢出(瞬时数据太大);⑤ 泄漏修代码,溢出加限流/分页/流式处理。4G1 和 CMS 区别? 中等
CMS: 老年代并发收集器,低停顿,但有碎片(标记清除)、并发模式失败风险,JDK9 废弃。G1: 分 Region,可预测停顿时间(-XX:MaxGCPauseMillis),适合大堆,JDK9+ 默认。G1 整体更优,是现代首选。
5Minor GC 和 Full GC 区别? 基础
Minor GC: 清新生代(Eden+S0),频繁、快、STW 短。Full GC: 清整个堆(新生代+老年代),慢、STW 长、要尽量避免。Full GC 频繁说明:老年代不够、内存泄漏、大对象直接进老年代。
6堆参数怎么设置? 中等
-Xms 初始堆 = -Xmx 最大堆(避免动态扩容抖动)。一般服务器留一半给 OS 和堆外。新生代 -Xmn 一般堆的 1/3~1/2。生产 JDK9+ 直接用 G1(-XX:+UseG1GC),设 -XX:MaxGCPauseMillis=200。别过度调优,默认 G1 很合理。
7
30 秒陈述
🎤 30 秒版
「JVM 我处理过线上 OOM。报表导出一次查 50 万条全放内存堆撑爆,我用 jmap dump + MAT 看 Dominator Tree,定位 ArrayList 占 80%,改流式处理解决。JVM 内存我清楚:堆(对象)+ 元空间(JDK8+,类信息)+ 栈 + 计数器。GC 分代,新生代用复制算法老年代用整理,G1 分 Region 可预测停顿。生产配 HeapDumpOnOutOfMemoryError 自动 dump,别等出事手动 jmap(STW)。