周三凌晨两点,服务被 OOM killer 干掉了,进程直接消失,监控上内存曲线是一根完美的爬坡线:每次发版后掉下去,然后花四天爬到 95%。标准的内存泄漏形态。这篇记录完整定位过程,希望能给你一个可复用的套路。
第一步:拿到现场(heap dump)
可惜进程已经死透了,现场没了。给 JVM 加了两个参数,让下次 OOM 时自动留 dump:
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/data/dump/
另外用 jmap 手动抓了两份不同时间点的快照做对比:
jmap -dump:format=b,file=heap1.hprof 21409
第二步:MAT 分析
用 Eclipse MAT 打开 dump,直接看 Leak Suspects 报告。嫌疑对象一个 HashMap 占了 1.6G,里面塞了 200 多万个 Entry。点开支配树,顺着 GC Root 引用链往上找,发现它是一个 static 的本地缓存 Map:
private static final Map<String, UserInfo> CACHE = new HashMap<>();
// 用的时候 put,没有淘汰逻辑,没有上限
每次用户访问往里 put 一条,key 是 uid。用户量几十万,每次发版重启清零,然后四天爬满 4G 堆。就这么简单。
第三步:修复
- 这种"只进不出"的 Map,直接换成 Caffeine:
maximumSize(100_000).expireAfterWrite(Duration.ofHours(1)),内存占用从 1.6G 掉到 300M 左右 - 顺手排查了其它 static 集合,又揪出一个存线程本地变量的 ThreadLocal 没在 finally 里 remove(线程池场景下这同样是泄漏源)
复盘清单
- OOM 自动 dump 参数应该是标配,成本几乎为零
- 内存监控要看"趋势"而不只是"阈值":持续爬坡的曲线比单点告警更早暴露问题
- MAT 的支配树(Dominator Tree)是定位泄漏的神器,比肉眼翻对象列表快得多
- 写缓存先想淘汰策略,没有淘汰策略的缓存都是定时炸弹

Comments 4 条评论
加参数让 OOM 自动 dump,成本几乎为零
看趋势不看阈值,这句很关键
static 的 Map 只进不出,经典泄漏
ThreadLocal 忘了 remove 也一样