Java内存泄漏本质是对象或类被GC Roots强引用而无法回收,需按“现象→区域→工具→引用链→修复”排查,重点关注老年代与Metaspace持续增长及七类典型泄漏模式。

Java 中类与对象的内存泄漏隐患,本质是对象或类因被 GC Roots 强引用而长期驻留,无法被回收。排查不是靠猜,而是按“现象 → 区域 → 工具 → 引用链 → 修复”链条推进。重点不在“有没有泄漏”,而在“谁在强留它”。
看老年代和 Metaspace 是否持续增长
老年代占用只增不减、Full GC 后无回落,大概率是堆内对象泄漏;Metaspace 占用一路飙升(比如从 11MB 涨到 489MB),且 Class Loaded 数量翻三倍(如 11,000 → 40,000+),基本锁定为类泄漏。两者常并存——比如高频反射会同时生成大量 GeneratedMethodAccessor 类 + 持有这些类的 DelegatingClassLoader,导致类和实例双双滞留。
抓关键工具组合,分层定位
分析 Java Spring Boot 后端 Controller 代码,自动生成 Markdown 格式的接口文档。当用户说“生成 Java 接口文档”、“生成 API 文档”、“从 Spring 代码生成文档”时触发。
-
jstat 实时盯 GC 趋势:
jstat -gcutil看老年代(OU)和元空间(MU)是否阶梯式上涨;若 OU > 80% 且每次 Full GC 回收量5s -
jmap 抓快照前先确认范围:
jmap -histo:live快速列出实例数最多的类,若HashMap$Node、ConcurrentHashMap$Node或自定义CacheManager排前三,优先怀疑静态缓存。 -
MAT 分析必须走 Dominator Tree + Path to GC Roots:打开
.hprof后,先看 “Leak Suspects Report” 自动标红项;再进 Dominator Tree 找内存占比最高的对象;最后右键 → “Path to GC Roots” → 勾选 “exclude weak/soft references”,看哪条强引用链把它死死拽住。
查七类典型泄漏模式,对号入座
- 静态集合无限膨胀:
static Map、static List只 add 不 remove,GC Roots(静态变量)终身持有。
✅ 改用WeakHashMap(key 是弱引用)或ConcurrentHashMap配合expireAfterWrite(Caffeine)。 - 监听器/回调未注销:注册了
addXXXListener却没配对removeXXXListener,监听器隐式持有所属对象。
✅ 在对象销毁生命周期(如destroy()、onDestroy())中显式清理。 - ThreadLocal 未清理:线程池复用线程,
ThreadLocal.set()后忘记remove(),值随线程长期存活。
✅ 总是配合try-finally使用:try { tl.set(x); ... } finally { tl.remove(); }。 - 内部类隐式持外部类:非静态内部类自动持有
this$0,若该内部类被静态变量或长生命周期对象引用,外部类也被拖住。
✅ 改为静态内部类 + 显式传参,或用WeakReference包装外部引用。 - 反射与内省泄漏:频繁调用
Method.invoke()或new PropertyDescriptor(),触发 JVM 生成GeneratedMethodAccessor类,由独立ClassLoader加载且永不卸载。
✅ 缓存Method对象复用;避免在高频路径上做运行时内省;用BeanUtils.copyProperties替代手动PropertyDescriptor。 - Gson/JSON 库重复创建:每次请求都
new Gson(),每个实例初始化几十个TypeAdapterFactory并缓存类型适配器。
✅ 全局单例复用Gson实例(注意线程安全),或用GsonBuilder().create()预构建。 - 缓存键未重写
equals/hashCode:ConcurrentHashMap中键对象(如 record 或 Lombok @Data 类)未正确实现,导致相同语义的键无法覆盖旧值,缓存持续膨胀。
✅ 所有作缓存 key 的类,必须确保equals和hashCode逻辑一致且稳定。
不复杂但容易忽略
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










