内存泄漏引发频繁Full GC的核心信号是老年代回收后占用率“只增不减”:OU单向上涨、GC后回落极少(2%~5%),OC稳定,FGC增多且YGC/FGC比值趋近1:1;需用jstat持续观察,jcmd+jmap轻量dump,MAT通过Dominator Tree和Histogram结合GC Roots路径精准定位,辅以Arthas运行态验证静态集合、ThreadLocal等典型泄漏点。

直接看老年代回收后是否“只增不减”——这是内存泄漏引发频繁 Full GC 最核心的信号。不是 GC 多就一定有问题,关键是每次 Full GC 后老年代占用率下降极少(比如只降 2%~5%),且趋势持续爬升,那基本可以锁定是泄漏。
第一步:确认是不是真泄漏,而不是配置或流量问题
用 jstat -gc <pid> 5s 持续观察,重点关注三列:
- OU(Old Used):老年代已用空间,看它是否单向上涨、GC 后回落幅度越来越小
- OC(Old Capacity):老年代总容量,应基本稳定;如果 OC 也在涨,可能是元空间或堆外内存异常
- FGC:Full GC 次数,结合 OU 趋势判断——次数增多 + OU 降不动 = 典型泄漏
同时对比 YGC/FGC 比值:健康应用通常 >10:1;若接近 1:1 或倒挂,说明大量对象活过多次 Young GC 直接进老年代,也可能是泄漏或大对象晋升异常。
第二步:抓一份轻量、可用的堆快照
别一上来就 jmap -dump 硬抓——大堆下容易卡住,生产环境风险高。优先用更稳妥的方式:
立即学习“Java免费学习笔记(深入)”;
- 加 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/data/dumps/ 提前埋点,等 OOM 自动触发
- 手动抓取时,用 jcmd <pid> VM.native_memory summary 先看内存压力类型;再执行 jmap -dump:format=b,live,file=heap.hprof <pid>,加 live 参数只导存活对象,文件小、分析快、STW 时间短
- 避免用 jmap -histo:live <pid>,它会强制触发一次 Full GC,可能加重线上压力
第三步:用 MAT 精准定位泄漏源头
MAT 不是用来数对象多少的,而是找“谁在强引用不该活的对象”。打开 heap.hprof 后,重点看两个视图:
- Dominator Tree → 按 Retained Heap 排序,找前三名大对象 → 右键 “Path to GC Roots” → 选 “with all references”,重点看业务类(如 CacheManager、UserService)是否意外持有了大量 byte[]、String 或集合
- Histogram → 找实例数暴增或单个对象特别大的类(比如 ArrayList、HashMap$Node、自定义缓存实体)→ 右键该类 → “List objects” → “with incoming references” → 对每个实例查 “Path to GC Roots”,确认引用链是否合理
注意:别迷信 “Leak Suspects Report”,它误报率高;真正关键的是你手动验证的 GC Root 强引用路径。
第四步:运行态验证典型泄漏模式
有些泄漏在堆快照里藏得深,或者只在特定逻辑路径下发生,需结合运行中行为确认:
- 静态集合膨胀:用 Arthas 执行 watch com.xxx.CacheService put '{params,returnObj}' -n 20,看缓存是否只写不删
- ThreadLocal 泄漏:检查线程池复用场景中,是否在 filter、异步任务里 set 了 ThreadLocal 却没 remove()
- 连接/流未释放:搜代码里 new Socket()、Connection、InputStream 等,确认是否都在 try-with-resources 或 finally 里 close
- 监听器未注销:注册事件、回调的地方,是否在对象销毁时显式 removeListener()
不复杂但容易忽略。


















