OOM排查需三步:看错误类型区分溢出区域,抓运行快照(堆转储、GC日志、jstat监控),用MAT分析强引用链定位泄漏源;修复应优先改代码而非调参。

遇到 JVM 内存溢出(OutOfMemoryError),关键不是立刻加内存,而是快速锁定“哪块内存满了”、弄清“为什么没被回收”、再决定“是调参还是改代码”。经验表明,90% 的 OOM 问题靠三步就能准确定位:看错误类型 → 抓运行快照 → 分析强引用链。
一、先看错误信息,区分溢出区域
不同 OOM 日志指向完全不同的排查路径,不能一概而论:
- Java heap space:堆内存不足,最常见。通常伴随频繁 Full GC 或老年代使用率持续 >95%,说明对象堆积或泄漏;
- Metaspace:类加载过多,比如用了大量动态代理、热部署未清理旧类、Spring Boot 多模块反复刷新上下文;
-
unable to create new native thread:线程数超限,不是堆不够,而是系统级线程资源耗尽,常因线程池未复用、异步任务无界提交、或
-Xss设置过大导致单线程栈占用太多; -
Direct buffer memory:NIO 的堆外内存爆了,
-Xmx完全不管它,得单独配-XX:MaxDirectMemorySize; - GC overhead limit exceeded:不是内存真不够,而是 GC 花了 98% 时间却只回收不到 2% 内存,往往预示着严重泄漏或对象生命周期异常延长。
二、现场快照要趁早,别等 OOM 才动手
OOM 发生时进程可能已卡死,很多命令执行不了。平时就要配置好自动采集机制:
- 启动参数加
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/dump/,确保出问题能拿到 .hprof 文件; - 加
-Xlog:gc*:gc.log:time输出结构化 GC 日志,比老式-XX:+PrintGCDetails更易分析停顿和晋升行为; - 定期用
jstat -gc <pid> 2000观察 O(老年代使用率)是否缓慢爬升、FGC 是否越来越密——这是泄漏的早期信号; - 怀疑线程问题时,用
ps -eLf | grep <pid> | wc -l查真实线程数,再对比ulimit -u系统限制。
三、MAT 分析不只看“谁占得多”,要看“谁不该活”
打开 .hprof 后,别直接翻大对象列表。真正有用的是这条路径:
- 跑 Leak Suspects Report,它会标出几个高风险对象组,优先点开;
- 进 Dominator Tree,按 Retained Heap 排序,找业务相关的类(如
UserService、CacheHolder),而不是char[]或HashMap$Node; - 对可疑实例右键 → Path to GC Roots → exclude all weak/soft references,只留强引用链——终点往往是
static字段、未清理的ThreadLocal、注册后没反注册的监听器、或线程池里长期持有的 Runnable 引用; - 特别注意集合类本身不泄漏,但里面没清理的元素才是元凶:比如缓存没设 TTL、日志队列没限流、回调 Map 没 remove。
四、修复要分层,参数只是兜底,代码才是根治
调参能缓解,但掩盖不了设计缺陷:
- 堆内存建议设
-Xms和-Xmx相同(如-Xms4g -Xmx4g),避免扩容抖动; - Metaspace 可设
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m,防止初期激增触发频繁 GC; - 线程相关,检查所有
new Thread()是否可改为线程池复用,Executors.newCachedThreadPool()必须替换成有界队列的定制池; - 代码层重点查:数据库查询是否一次拉百万条、文件流是否漏 close、监听器是否在销毁时 unregister、静态集合是否随请求不断 add 却不清理。

















