🧩 场景题题干(面试官常用表述)
线上现象:频繁 Full GC,RT 变慢,最终 OOM 重启。
你如何判断是内存泄漏还是正常负载?
如何定位到泄漏对象?如何修复并长期治理?
💡 一句话思路:先看 GC/堆使用趋势(是否“回收后仍上升”),再抓 heap dump 找“占用最大且一直增长”的对象链,最后回到代码与业务量解释根因。
📚 学习正文:从“现象”定位到“泄漏对象/配置不当/大对象”
交互图:OOM / Full GC 排查闭环(点步骤切换证据与动作)
目标是形成“证据闭环”:现象(GC/指标)→ 是谁(dump)→ 为什么(引用链/代码)→ 怎么治(短期止血 + 长期治理)。
1) 确认现象类型(先分桶,避免方向错)
- 看报错信息:Java heap space / Metaspace / Direct buffer memory / GC overhead limit。
- 看 Full GC 频率:是“突然暴涨”(发布/流量)还是“持续恶化”(泄漏/数据膨胀)。
- 第一口径:先明确目标:恢复可用(止血)还是定位根因(抓证据)。
2) 看趋势判泄漏(最关键的判断:GC 后是否还在涨)
- 锯齿基线抬升:每次 GC 后最低点越来越高 → 典型泄漏/缓存膨胀。
- Full GC 收益很小:回收后释放很少 → 存活对象过多(老年代压力大)。
- 关联业务量:流量/数据量增长导致合理增长 ≠ 泄漏(要结合 QPS/数据量)。
3) 抓证据(没有证据,优化就是“猜”)
- GC 日志:回答“发生了什么”(频率/耗时/回收效果)。
- Heap Dump:回答“是谁占用”(类/对象/支配树/引用链)。
- 线程栈:回答“谁在分配/谁在持有”(导出/批处理/大查询/循环)。
常用抓取命令(示意):
1) jcmd
GC.heap_info
2) jcmd GC.class_histogram
3) jcmd GC.heap_dump /tmp/heap.hprof
4) jstack > /tmp/jstack.txt
4) 定位“是谁占用”(先找最大头)
- Top Consumers:哪个类实例占用最多(数量/总大小)。
- Dominator Tree:谁支配了大量对象(删掉它,下面才会释放)。
- 对比两次 dump:隔一段时间抓两份,找“持续增长”的类更像泄漏根因。
5) 定位“为什么存活”(沿 GC Roots 找到持有者)
- 常见持有者:静态 Map/缓存、ThreadLocal、队列堆积、listener 未注销、集合未清理。
- 落到代码:引用链最终要能解释为“谁把对象放进去、为何不移除、生命周期是否合理”。
- 验证:修复后再压测/回放,对比 OldGen 基线与 Full GC 频率。
6) 修复与治理(别只靠“加大堆”)
- 止血:限流/降级/摘流,必要时临时扩容,但要同步抓证据避免掩盖泄漏。
- 修复:缓存上限+淘汰、对象生命周期梳理、避免无界队列/无界 Map。
- 治理:GC 指标告警(FullGC 次数/耗时、OldGen 占用)、上线评审与压测回归。
1) 先止血:避免连续 Full GC 把服务拖死
止血优先级:摘流异常实例 → 降级/限流 → 回滚发布 → 扩容(谨慎,避免掩盖泄漏)
目标:让服务可用,争取时间抓证据(GC 日志/heap dump)。
2) 证据链:判断是不是“泄漏趋势”
- 锯齿基线抬升:每次 GC 后最低点越来越高(典型泄漏/缓存膨胀)。
- Full GC 收益很小:Full GC 后释放很少,说明存活对象过多。
- 关联业务量:流量/数据量增长导致堆使用合理增长 ≠ 泄漏。
3) 抓证据:GC 日志 + Heap Dump + 引用链
GC 日志回答“发生了什么”(频率/耗时/回收效果)
Heap Dump 回答“是谁占用”(Top Consumers/Dominator Tree/GC Roots 引用链)
4) 常见 OOM 类型与定位方向
- Java heap space:对象过多/泄漏/大对象。
- Metaspace:类加载过多、动态生成类、类卸载异常。
- Direct buffer memory:NIO/Netty 直接内存上限或释放问题。
- GC overhead limit:GC 占用时间过高但回收很少,常见于堆压力/泄漏。
5) 修复与治理
短期:扩大堆/降低峰值/降级/回滚(视业务)
长期:缓存上限+淘汰、对象生命周期梳理、GC 指标告警、压测与回归、定期泄漏演练