目标
内存排查常败在拿苹果比橘子。在同一工作负载阶段、只隔着增长间隔取两个堆快照,恰好显示出哪个对象群体在膨胀;保留链再点名是谁在让它们活着。要修的是保留机制(那个从不解绑的 close 监听器),而不是受害的对象本身(把 Session 改小)。
步骤要点
- 先确认是泄漏不是缓存:稳态负载下画内存随时间的曲线——泄漏单调增长、缓存趋于平台、GC 压力往复振荡,形状决定打法。
- 在增长曲线上取两个同阶段快照:同一工作负载阶段相隔一段增长取堆快照,对比保留大小。
- 二分保留路径:沿增长增量的保留链上溯——哪个 Map、监听器注册表或闭包在对象失效后仍持住它们。
- 修复保留机制:解绑监听器、给 Map 加 LRU 上界、关闭资源或清理持有闭包的定时器。
- 用浸泡运行验证:修复前后各跑数小时持续负载,堆曲线保持平稳且进程活过预期运行时长。
排错提示
两个快shot差着工作负载阶段比出来的全是噪音,务必同阶段对比;修了对象没修机制泄漏照样活,按保留链修到持有人;快照平坦就收工为时尚早,慢泄漏或突发阶段只有整段时长的浸泡负载才能证伪。
适用边界
堆快照本身会暂停进程,生产环境要摘流量后抓取或用低开销采样替代;原生内存(Buffer、堆外)不在对象图里,需进程级 RSS 与工具配合分析。
代码示例
trend (steady 200 rps, 1h): RSS 380 -> 742 MB monotonic = LEAK
snapshot A (after 10 min) vs snapshot B (after 60 min)
delta, by retained size:
Session objects +38,114 (4.1MB -> 212MB)
retainer chain:
Session -> sessionCache Map -> disconnect handler
-> socket close listener never removed
fix: registry.unbind(socket) in close handler
+ LRU bound on sessionCache (10k)
snapshot C vs D (post-fix, same protocol):
Session delta: +41 (noise) FLAT
soak (intended uptime 72h): RSS 380 -> 391MB PASS