OOM 排查与堆转储分析

系统讲解 OOM 排查与堆转储分析:五种 OOM 类型与成因、排查流程、jmap 与自动转储获取堆转储、MAT 的 Leak Suspects 与 Dominator Tree 分析、内存泄漏定位案例,以及线上兜底与预防手段。

OOM(OutOfMemoryError)是生产环境最惊心动魄的故障之一:进程可能直接崩溃,也可能在频繁 Full GC 中「假死」。排查 OOM 不能靠猜,靠的是堆转储 + 引用链分析的完整证据链。本文从 OOM 类型讲起,覆盖转储获取、MAT 分析、泄漏定位与线上兜底,帮你把「崩溃现场」变成「可复现的结论」。

一、OOM 的类型与成因

1.1 五种常见 OOM

类型抛错场景典型成因
Java heap space堆内存不足大对象、泄漏、堆太小
GC overhead limit exceededGC 超过阈值仍收不回内存内存将尽,GC 空转
Metaspace元空间不足类加载过多(反射/代理泄漏)
Unable to create native thread无法创建系统线程线程数达系统上限或内存不足
Direct buffer memory堆外内存不足DirectBuffer 未释放
关键词对比:
  heap space      → 堆内对象太多
  native thread   → 线程太多(每线程 1MB 栈)
  direct buffer   → 堆外内存泄漏
  metaspace       → 类/元数据泄漏(动态代理、反射)

1.2 泄漏 vs 内存不足

// 内存泄漏:对象不再使用,却被强引用链挂住,GC 收不走
public class LeakHolder {
    static final List<byte[]> CACHE = new ArrayList<>();
    void store(byte[] data) { CACHE.add(data); }  // 只进不出 → 泄漏
}

// 内存不足:对象被正常使用,但总量超过堆上限(一次性大请求)
// 与泄漏的本质区别:内存不足清掉请求就恢复,泄漏清不掉

一句话总结: 先分清五种 OOM 的「报错位置」(堆/元空间/线程/堆外),再判断是泄漏(收不回)还是不足(总量超限)——方向对了,排查才不走弯路。


二、OOM 排查流程

2.1 观察指标定方向

排查第一步:看监控指标
  1. 堆使用率曲线:持续上涨不回落 → 泄漏
  2. GC 频率与停顿:Full GC 越来越频繁 → 内存将尽
  3. 线程数曲线:持续上涨 → native thread OOM
  4. 容器内存/堆外:进程 RSS 远超堆大小 → 堆外泄漏

第二步:确认报错堆栈
  报错信息里的"谁在分配、分配多少"往往直接指向泄漏点

2.2 排查路线图

Java heap space / GC overhead
  → 收集堆转储 → MAT 找泄漏根因
Unable to create native thread
  → 看线程数 + 系统 ulimit → 线程泄漏或栈设置
Direct buffer memory
  → 查 Netty/自管理 DirectBuffer 的释放路径
Metaspace
  → 查动态代理/反射/类加载器泄漏

一句话总结: 排查不是从代码开始,而是先从监控曲线确认「泄漏型」还是「突发型」,再按 OOM 类型选择对应的转储和分析工具。


三、堆转储的获取

3.1 jmap 手动转储

# 方式一:jmap 直接 dump(会触发 Full GC,慎用于大堆)
jmap -dump:live,format=b,file=/tmp/heap.bin <pid>

# 方式二:HSDB / jcmd(推荐,命令一致)
jcmd <pid> GC.heap_dump /tmp/heap.bin
注意事项:
  jmap 默认会执行一次 Full GC,生产大堆慎用
  优先用 jcmd GC.heap_dump,行为更可控
  转储文件可能很大(等于堆大小),留足磁盘

3.2 选择 dump 时机

时机适用说明
OOM 瞬间自动 dump生产必备-XX:+HeapDumpOnOutOfMemoryError
定时快照缓慢泄漏定时 dump,MAT 对比增长
告警触发 dump高水位预警堆使用率超阈值时执行
压测复现后 dump测试环境复现问题后手动转储
缓慢泄漏的采样技巧:
  堆上涨初期 dump 一份(baseline)
  上涨明显后再 dump 一份(current)
  两次对比 → 新增存活对象 = 泄漏主体

3.3 自动转储:OOM 时自动留证

# 启动参数:OOM 时自动 dump + 自动退出,便于监控重启
java -XX:+HeapDumpOnOutOfMemoryError \
     -XX:HeapDumpPath=/data/dumps/ \
     -XX:OnOutOfMemoryError="kill -9 %p" \
     -Xmx4g MyApp
// 代码级兜底:捕获 OOM 记录上下文,便于分析
try {
    // 大对象分配
} catch (OutOfMemoryError e) {
    log.error("OOM 上下文: requestId={}, userId={}", reqId, userId, e);
    throw e;   // 让 JVM 的 HeapDumpOnOutOfMemoryError 继续生效
}

一句话总结: 手动转储用 jcmd GC.heap_dump,生产一定要配 -XX:+HeapDumpOnOutOfMemoryError 自动留证——崩溃现场是最宝贵的排障资产。


四、MAT 分析实战

4.1 Leak Suspects:自动找泄漏嫌疑

MAT(Memory Analyzer)打开 heap.bin 后,Leak Suspects 报告直接列出最可能的泄漏点。

Leak Suspects 报告结构:
  Suspect 1:某个对象占了多少堆,被谁全局持有
  详情:Dominator Tree 中的路径 → 引用链 → 泄漏根因

解读要点:
  报告给出的是"最大的堆占用者",不一定是业务泄漏
  结合业务代码判断:这个大对象本该释放吗?

4.2 Dominator Tree:支配树看引用

Dominator Tree(支配树):
  每个节点的"支配者"是持有它、且其被释放则它必然被释放的对象
  顺着支配树看:谁独占了大头 → 谁持有不释放

常用视图:
  Histogram:按类统计实例数与占用
  Path to GC Roots:某个对象的完整引用链
  Retained Heap:对象及其支配子树占用的内存
// 经典泄漏根因示例:静态集合 + 业务对象
public class CacheManager {
    // static 引用 → GC Roots 可达 → 永不释放
    private static final Map<String, Session> SESSIONS = new HashMap<>();
}
// MAT 中:SESSIONS → Path to GC Roots 显示 "static final"
// 说明缓存只进不出是泄漏源

4.3 其他 MAT 视图与技巧

视图/操作用途
Histogram按类统计实例数与 Shallow Heap
Thread Overview看线程各自持有的堆对象
Top Consumers按包/类加载器聚合占用
Find Leaks(报告)自动生成 Leak Suspects 报告
Compare Snapshots两次 dump 对比,看增长对象
Open Query BrowserOQL 自定义查询
OQL 示例:找所有超过 1MB 的 byte[] 实例
  SELECT * FROM byte[] b WHERE b.length > 1048576

对比两个 dump(间隔一段时间):
  MAT → Compare → 找出"新增且存活"的对象
  往往就是正在泄漏的那批

一句话总结: MAT 的 Leak Suspects 给出嫌疑名单,Dominator Tree 给出引用链;把「大对象」和「Path to GC Roots」对上,泄漏根因就浮出水面了;两次 dump 对比是定位缓慢泄漏的利器。


五、泄漏定位案例

5.1 案例:缓存只进不出

现象:堆曲线持续上涨,Full GC 越来越频
Leak Suspects:一个 ConcurrentHashMap 占用 60% 堆

分析路径:
  1. Histogram 找最大类 → ConcurrentHashMap
  2. Path to GC Roots → 静态字段引用的本地缓存
  3. 查代码 → 缓存 key 永不失效,value 越积越多
修复:
  加 TTL 淘汰 / 改用带过期策略的缓存框架

5.2 案例:线程局部 ThreadLocal 泄漏

现象:thread 数不多,但堆上涨

根因:ThreadLocal 值被线程持有,线程长期存活不销毁
  → value 一直可达,GC 收不掉

识别:
  Histogram 中 ThreadLocal 相关类实例数与线程数对得上
  Thread 的 threadLocals 指向一个巨大 value

修复:用完 remove(),或用作用域内清理的封装
// 正确姿势:ThreadLocal 用完必须 remove
ThreadLocal<BigObject> tl = new ThreadLocal<>();
try {
    tl.set(bigObject);
    doWork();
} finally {
    tl.remove();   // 关键:清掉,防止线程池复用串值
}

一句话总结: 泄漏案例分析的两个常见剧本是「缓存只进不出」和「ThreadLocal 挂大对象」;MAT 提供证据,修复要点是给缓存加过期、给 ThreadLocal 加 remove。


六、常见内存泄漏类型汇总

泄漏类型特征MAT 证据对策
静态集合静态 Map/List 无限增长静态字段 GC Root加淘汰策略
ThreadLocal线程存活 + value 不清理Thread 的 threadLocals用完 remove
监听器/回调注册后未反注册集合持有业务对象配对 register/unregister
类加载器泄漏自定义 CL 反复加载Metaspace 增长复用 CL,卸载可回收
连接未关闭IO/DB 连接泄漏连接对象堆积try-with-resources
缓存框架配置无过期/无上限缓存实例巨大配置 TTL/容量
预防思路(前置):
  1. 设置合理的堆上限,配合 GC 日志
  2. 对缓存类对象做容量/过期约束
  3. 统一资源关闭(连接、流)规范
  4. 压测阶段引入泄漏检查(如 JFR + MAT 定期体检)

一句话总结: 泄漏的六个常见剧本都有固定的 MAT 证据形态;把「静态、ThreadLocal、监听器、类加载器、连接、缓存」六类检查点建成代码评审清单,能拦住大多数泄漏。


七、线上兜底与预防

7.1 OOM 时的兜底策略

崩溃恢复兜底:
  1. 进程自动重启(systemd / k8s 重启策略)
  2. 重试机制:上游调用方重试,规避一次性大请求
  3. 限流降级:OOM 前先熔断,保护后续请求
  4. 分片分页:大结果集分批处理,避免一次堆满

预防兜底:
  1. 配 HeapDumpOnOutOfMemoryError,自动留证
  2. 监控堆使用率与 GC,设阈值告警
  3. 定期用 JFR/MAT 做内存健康检查

7.2 启动参数参考

# 一个兼顾"兜底 + 留证"的启动配置示例
java \
  -Xmx4g -Xms4g \
  -XX:+HeapDumpOnOutOfMemoryError \
  -XX:HeapDumpPath=/data/dumps/ \
  -XX:+ExitOnOutOfMemoryError \
  -Xlog:gc*:file=/data/logs/gc.log \
  MyApp

一句话总结: 线上兜底三件事——自动转储留证、自动重启恢复、监控告警提前干预;OOM 不可能 100% 杜绝,但可以做到「崩溃能分析、恢复能自动」。


八、实战陷阱清单

陷阱现象对策
jmap 触发 Full GC大堆抖动、停顿用 jcmd,低峰期执行
磁盘不足dump 失败预留堆大小 1.5 倍空间
dump 后忘下载崩溃现场丢失转储目录自动归档
只看 Histogram定位不准看 Path to GC Roots
误把大对象当泄漏冤案结合业务判断是否本应释放
ThreadLocal 不清理泄漏缓慢强制 remove 规范
只修内存不足不防泄漏复发建立泄漏检查清单

九、总结

阶段工具产出
定方向监控曲线 + 报错堆栈泄漏型 or 突发型
留证据HeapDumpOnOutOfMemoryError崩溃现场转储
找根因MAT Leak Suspects / Dominator Tree引用链与泄漏点
兜底自动重启 + 限流降级恢复可用
预防GC 日志 + 定期体检早发现早处理

一句话记住:OOM 排查的本质是把「内存崩溃」变成「引用链证据」。先看曲线分类型,再自动留证,最后用 MAT 的支配树找到谁该释放却没释放——配好自动转储和监控告警,OOM 就不再是「看天意」的故障。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. Java 序列化方案对比与性能
  2. Java 并发设计模式实战
  3. Arthas 线上诊断实战