内存泄漏导致频繁Full GC的典型特征是老年代使用量(OU)单向爬升且每次Full GC后回落幅度持续减小;可用jstat -gc 5s观察OU、OC趋势,结合jcmd或带live参数的jmap抓取堆快照,再用MAT的Dominator Tree和Histogram定位强引用链。

怎么看是不是内存泄漏导致的频繁 Full GC
频繁 Full GC 本身不是问题,问题是它反复发生且老年代回收后存活对象持续增长——这大概率是内存泄漏。先别急着调参数,用 jstat 看趋势:jstat -gc <pid> 5s</pid>,重点关注 OU(老年代使用量)是否单向爬升、OC(老年代容量)是否稳定,以及每次 Full GC 后 OU 下降幅度越来越小。
- 如果
OU每次只回落一点点(比如从 2.8G → 2.75G → 2.72G),基本可以锁定泄漏 - 如果
OU回落到低位后又快速回升,可能是缓存没设上限或对象生命周期管理失控,不一定是泄漏 -
jmap -histo:live <pid></pid>能看当前活跃对象数量,但注意:它会触发一次 Full GC,生产环境慎用
怎么抓有效的堆快照(heap dump)不拖垮应用
直接 jmap -dump:format=b,file=heap.hprof <pid></pid> 在大堆(>4G)上容易卡住几秒甚至分钟,可能引发超时雪崩。更稳妥的方式是让 JVM 自动在 OOM 时 dump,或者用 jcmd 触发无阻塞快照(JDK 8u60+):
- 启用自动 dump:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dumps/ - 手动抓取(低干扰):
jcmd <pid> VM.native_memory summary</pid>先确认内存压力类型;再用jcmd <pid> VM.native_memory detail</pid>排查本地内存异常 - 真正要 dump 堆时,优先选
jcmd <pid> VM.native_memory summary</pid>+jmap -dump:format=b,live,file=heap.hprof <pid></pid>,加live参数可跳过 unreachable 对象,文件更小、分析更快
用 MAT 分析 heap.hprof 时最该盯哪几个视图
MAT 不是用来“看对象多不多”的,而是定位“谁在强引用不该活的对象”。打开 dump 后,别急着点 “Leak Suspects Report”,那个报告经常误报。直接进这几个关键路径:
分析 Java Spring Boot 后端 Controller 代码,自动生成 Markdown 格式的接口文档。当用户说“生成 Java 接口文档”、“生成 API 文档”、“从 Spring 代码生成文档”时触发。
- “Dominator Tree” → 按 “Retained Heap” 降序,找排前三的大对象,右键 “Path to GC Roots” → 选 “with all references”,重点看有没有业务类(比如
UserService、CacheManager)意外持有了大量byte[]或String - “Histogram” → 右键某个可疑类(如
ArrayList),选 “List objects” → “with incoming references”,再对每个实例点 “Path to GC Roots”,看是谁把它挂住了 - 注意 “Thread Objects” 分组:线程没结束,它的栈帧和局部变量就一直 hold 对象,常见于线程池里忘了清理
ThreadLocal的 value
常见泄漏模式和对应修复动作
90% 的泄漏不是代码写错,而是资源生命周期没对齐。下面这些模式在堆快照里一搜一个准:
立即学习“Java免费学习笔记(深入)”;
-
ThreadLocal泄漏:子线程复用线程池时,父线程 set 的值没 remove,导致ThreadLocalMap里 key 为 null、value 却一直存在 → 修复:所有ThreadLocal.set()后必须配对remove(),尤其在finally块中 - 静态集合缓存无淘汰:比如
private static final Map<string object> cache = new HashMap();</string>→ 修复:换成ConcurrentHashMap+ 定期清理,或直接用Caffeine.newBuilder().maximumSize(1000).build() - 监听器/回调未注销:GUI 或 Spring 事件监听器注册后没解绑 → 修复:检查
addXXXListener是否有对应removeXXXListener,Spring 中优先用@EventListener+@PreDestroy - 内部类持有外部类引用:非静态内部类隐式持外部类 this,若被长生命周期对象(如线程、缓存)引用,整个外部类无法回收 → 修复:改用静态内部类 + 显式传参,或用
WeakReference
堆快照里看到大量 java.lang.ref.Finalizer 实例,说明对象重写了 finalize() 且队列积压——这是典型的设计反模式,直接删掉 finalize 方法比任何调优都管用。

















