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

怎么看是不是内存泄漏导致的频繁 Full GC
频繁 Full GC 本身不是问题,问题是它反复发生且老年代回收后存活对象持续增长——这大概率是内存泄漏。先别急着调参数,用 jstat 看趋势:jstat -gc ,重点关注 OU(老年代使用量)是否单向爬升、OC(老年代容量)是否稳定,以及每次 Full GC 后 OU 下降幅度越来越小。
- 如果
OU每次只回落一点点(比如从 2.8G → 2.75G → 2.72G),基本可以锁定泄漏 - 如果
OU回落到低位后又快速回升,可能是缓存没设上限或对象生命周期管理失控,不一定是泄漏 -
jmap -histo:live能看当前活跃对象数量,但注意:它会触发一次 Full GC,生产环境慎用
怎么抓有效的堆快照(heap dump)不拖垮应用
直接 jmap -dump:format=b,file=heap.hprof 在大堆(>4G)上容易卡住几秒甚至分钟,可能引发超时雪崩。更稳妥的方式是让 JVM 自动在 OOM 时 dump,或者用 jcmd 触发无阻塞快照(JDK 8u60+):
- 启用自动 dump:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dumps/ - 手动抓取(低干扰):
jcmd先确认内存压力类型;再用VM.native_memory summary jcmd排查本地内存异常VM.native_memory detail - 真正要 dump 堆时,优先选
jcmd+VM.native_memory summary jmap -dump:format=b,live,file=heap.hprof,加live参数可跳过 unreachable 对象,文件更小、分析更快
用 MAT 分析 heap.hprof 时最该盯哪几个视图
MAT 不是用来“看对象多不多”的,而是定位“谁在强引用不该活的对象”。打开 dump 后,别急着点 “Leak Suspects Report”,那个报告经常误报。直接进这几个关键路径:
将后端 Java 接口文档转换为前端 TypeScript 类型定义(interface / type)。输入:飞书文档、Spring Boot 接口文档、Swagger/OpenAPI、手写 API 文档等。输出:包含请求参数与响应结果的 .md 文件,含完整 JSDoc 注释。触发关键词:Java 转 TS、接口转换、生成前端类型、接口文档转 TypeScript、DTO/VO/BO 转 interface、后端文档转前端类型、生成接口类型、类型定义生成。
- “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% 的泄漏不是代码写错,而是资源生命周期没对齐。下面这些模式在堆快照里一搜一个准:
-
ThreadLocal泄漏:子线程复用线程池时,父线程 set 的值没 remove,导致ThreadLocalMap里 key 为 null、value 却一直存在 → 修复:所有ThreadLocal.set()后必须配对remove(),尤其在finally块中 - 静态集合缓存无淘汰:比如
private static final Map→ 修复:换成cache = new HashMap(); ConcurrentHashMap+ 定期清理,或直接用Caffeine.newBuilder().maximumSize(1000).build() - 监听器/回调未注销:GUI 或 Spring 事件监听器注册后没解绑 → 修复:检查
addXXXListener是否有对应removeXXXListener,Spring 中优先用@EventListener+@PreDestroy - 内部类持有外部类引用:非静态内部类隐式持外部类 this,若被长生命周期对象(如线程、缓存)引用,整个外部类无法回收 → 修复:改用静态内部类 + 显式传参,或用
WeakReference
堆快照里看到大量 java.lang.ref.Finalizer 实例,说明对象重写了 finalize() 且队列积压——这是典型的设计反模式,直接删掉 finalize 方法比任何调优都管用。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










