一次线上内存泄漏排查:从 GC 日志到代码定位
一次线上内存泄漏排查:从 GC 日志到代码定位
现象
服务跑了三天,RSS 从 400MB 涨到 2.1GB,重启后恢复正常,但三天后又会重演。监控上 CPU 正常、QPS 正常、延迟只有轻微上升,属于典型的"慢性泄漏"。
第一步:确认是堆内还是堆外
先看 JVM 的老年代占用曲线:老年代缓慢上升、Full GC 后也降不下来,说明是堆内泄漏;如果堆稳定但 RSS 涨,那就是堆外(DirectByteBuffer、线程栈、native 库)。
这个案例里堆内堆外都在涨,先按堆内查。
第二步:抓两次堆快照做对比
jmap -dump:live,format=b,file=heap1.hprof <pid> # 等 30 分钟 jmap -dump:live,format=b,file=heap2.hprof <pid>
用 MAT 打开两个快照做 Histogram 对比,按 retained size 排序。第二次快照里 `java.util.concurrent.LinkedBlockingQueue$Node` 多了 120 万个实例,基本锁定了方向。
第三步:找引用链
对某个队列节点做 "Path to GC Roots",发现引用链挂在一个静态 Map 上:
ThreadPoolExecutor$Worker -> Task -> CallbackHolder -> static Map<String, Task>
看代码:任务完成后只从线程池里移除,忘了从 `TaskRegistry` 这个静态 Map 里删除。每个请求注册一个任务,一天下来十几万条永远不释放。
第四步:修复与验证
// before
registry.put(taskId, task);
executor.submit(task);
// after
registry.put(taskId, task);
executor.submit(() -> {
try {
task.run();
} finally {
registry.remove(taskId); // 关键
}
});
改完压测 72 小时,RSS 稳定在 520MB,Full GC 后回到 380MB,不再单调上涨。
几条经验
• 静态集合是内存泄漏的头号嫌疑人,尤其是"注册表"类的结构
• 抓快照要抓两次,单次快照只能看总量,看不出增长
• `-XX:+HeapDumpOnOutOfMemoryError` 一定要开,事后救命
• 上线前用 `-Xmx` 压住上限,让问题早暴露而不是拖到 OOM
小结
内存泄漏的排查路径其实很固定:先分清堆内外,再用两次快照锁定增长对象,最后顺引用链找到根。真正耗时的不是技术,而是等待它涨到能看出差异的那几个小时。
- 点赞
- 收藏
- 关注作者
评论(0)