OOM 如何排查和解决?

2025年 阅读约 9 分钟 面试指南 · Java面试 · JVM

深入解析OOM排查方法:jmap生成dump、MAT分析堆转储、jstack查看线程、常见OOM类型(堆溢出、元空间溢出、GC overhead、直接内存溢出)及解决方案,附完整实战案例和面试模拟。

一句话总结

OOM 排查四步法:① 看日志(确认 OOM 类型和发生时机)→ ② 导 dump(jmap -dump 或 -XX:+HeapDumpOnOutOfMemoryError 自动导出)→ ③ 分析 dump(MAT/Eclipse Memory Analyzer 查看大对象、GC Roots 引用链、泄漏嫌疑报告)→ ④ 定位修复(根据分析结果修复代码或调整 JVM 参数)。常见 OOM 类型:Java heap space、GC overhead limit exceeded、Metaspace、Direct buffer memory。

初级理解

四种常见 OOM 类型

OOM 类型错误信息常见原因解决方向
堆溢出Java heap space对象太多、内存泄漏增大堆 / 排查泄漏
GC overheadGC overhead limit exceededGC 耗时超过 98% 但回收不到 2%增大堆 / 优化代码
元空间溢出Metaspace动态生成大量类增大元空间 / 限制代理
直接内存溢出Direct buffer memoryNIO 分配过多堆外内存增大直接内存 / 及时释放

排查工具速览

工具用途命令示例
jps查看 Java 进程 PIDjps -l
jmap导出堆 dump / 查看对象统计jmap -dump:format=b,file=heap.hprof <pid>
jstack查看线程堆栈jstack <pid>
jstat查看 GC 和内存统计jstat -gc <pid> 1000
MAT分析 dump 文件GUI 工具,查看泄漏报告

中级深入

自动导出 dump 配置

# OOM 时自动导出堆 dump -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump/ # 手动导出堆 dump jmap -dump:live,format=b,file=heap.hprof <pid> # live 参数:导出前先触发一次 Full GC,只保留存活对象 # 查看堆中对象统计(Top N) jmap -histo:live <pid> | head -20

MAT 分析四步法

// ① 打开 dump 文件 → 选择 Leak Suspects Report // ② 查看 Pie Chart:哪个对象占用最多内存 // ③ 查看 Dominator Tree:按保留大小排序,找大对象 // ④ 查看 GC Roots 路径:为什么这个对象不能被回收 // MAT 关键概念: // Shallow Heap:对象自身占用的内存 // Retained Heap:对象自身 + 它引用的所有对象的内存(回收它能释放的内存) // Dominator Tree:支配树,展示对象的保留关系

常见内存泄漏场景

// ① ThreadLocal 未 remove ThreadLocal<User> tl = new ThreadLocal<>(); tl.set(new User()); // 使用完未调用 tl.remove() // ② 集合类持有对象引用 static List<Object> list = new ArrayList<>(); // 静态集合不断 add,对象永远不会被回收 // ③ 内部类持有外部类引用 class Outer { // 非静态内部类默认持有外部类引用 // 如果内部类被长生命周期对象持有,外部类也无法回收 } // ④ 资源未关闭 InputStream is = new FileInputStream("file"); // 未调用 close(),关联的 native 内存未释放

高级拓展

GC Overhead Limit Exceeded 详解

// 触发条件:GC 时间超过总运行时间的 98%,但回收的内存不到 2% // 本质:JVM 在"垂死挣扎",大部分时间都在 GC,但几乎回收不了内存 // 解决方案: // 1. 增大堆内存 -Xmx // 2. 排查内存泄漏(dump 分析) // 3. 检查是否有超大对象 // 4. 关闭此检查(不推荐):-XX:-UseGCOverheadLimit

直接内存溢出排查

// 直接内存不在堆中,dump 文件看不到 // 排查方法: // 1. 检查 -XX:MaxDirectMemorySize 设置 // 2. 检查是否有大量 DirectByteBuffer 未释放 // 3. 使用 jcmd <pid> VM.native_memory summary 查看 native 内存 // 常见原因: // - NIO 使用 DirectByteBuffer 未及时释放 // - Netty 等框架使用堆外内存池 // - Unsafe.allocateMemory() 未释放

实战场景

完整排查流程

# 1. 查看进程 jps -l # 12345 com.example.MyApplication # 2. 查看内存和 GC 情况 jstat -gc 12345 1000 10 # 观察 EU、OU 是否持续增长,FGC 是否频繁 # 3. 查看对象统计 jmap -histo:live 12345 | head -30 # 看哪个类的实例数量异常多 # 4. 导出 dump jmap -dump:live,format=b,file=heap.hprof 12345 # 5. MAT 分析 # 打开 heap.hprof → Leak Suspects Report # 定位到泄漏对象 → 查看 GC Roots 路径 → 修复代码

内存泄漏代码示例

// 经典泄漏:HashMap 的 key 使用可变对象 public class MemoryLeak { public static void main(String[] args) { Map<User, String> map = new HashMap<>(); User user = new User("张三"); map.put(user, "value"); user.setName("李四"); // 修改了 key 的 hashCode! map.get(user); // null!找不到,但对象还在 map 中,无法回收 } }

面试模拟

Q:线上 OOM 如何排查?

A:1) 看日志确认 OOM 类型;2) 导 dump(jmap 或自动导出);3) MAT 分析:Leak Suspects Report → Dominator Tree 找大对象 → GC Roots 路径定位泄漏点;4) 修复:代码修复或调整 JVM 参数。同时用 jstat 观察 GC 趋势,jstack 查看是否有死锁或线程堆积。

Q:常见的内存泄漏场景有哪些?

A:1) ThreadLocal 未 remove;2) 静态集合不断 add 对象;3) 非静态内部类持有外部类引用;4) 资源未关闭(连接、流);5) 监听器/回调未注销;6) HashMap 使用可变对象作 key 导致无法 remove。

Q:GC overhead limit exceeded 是什么?怎么解决?

A:GC 耗时超过总运行时间的 98% 但回收不到 2% 内存时触发。说明 JVM 在"垂死挣扎"。解决:1) 增大堆内存;2) 排查内存泄漏;3) 检查是否有超大对象;4) 优化代码减少对象创建。不建议关闭此检查,那是掩耳盗铃。