一次线上内存泄漏排查:从 GC 日志到代码定位

举报
yd_270103096 发表于 2026/11/12 00:22:02 2026/11/12
【摘要】 从 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

小结

内存泄漏的排查路径其实很固定:先分清堆内外,再用两次快照锁定增长对象,最后顺引用链找到根。真正耗时的不是技术,而是等待它涨到能看出差异的那几个小时。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。