如何高效解决 Java 内存泄漏问题 – 目前最实用、最成体系的方法论
(强烈建议收藏)
先把最核心的排查顺序和思维路径记住(非常重要!)
最推荐的内存泄漏排查 大致优先级顺序(由快→慢,由大概率→小概率)
1. 看 GC 日志中哪个区域一直涨得最凶(最重要!)
↓
2. 看是不是 堆内存里 **老年代持续上涨** 且 **几乎没有被回收**
↓
3. 看是不是 **堆外内存**(Direct ByteBuffer / Netty / Unsafe)在疯狂增长
↓
4. 确定是**老年代**问题 → 优先找「**大量的长生命周期对象**」
↓
5. 找「**被长生命周期对象强引用的集合**」 → 最最最常见的元凶
↓
6. 再找「**各种缓存没有过期策略/淘汰策略**」
↓
7. 再找「**线程、连接、资源没有正确关闭**」
↓
8. 再找「**各种 Listener、Observer、回调没注销**」
↓
9. 最后才考虑「**非常非常隐蔽的泄漏**」(ThreadLocal、classloader、JNI等)
目前业界最推荐的 8步排查法(强烈建议按这个顺序来)
| 步骤 | 主要目的 | 主要看什么 / 常用命令 | 命中率排序 |
|---|---|---|---|
| 1 | 先判断是不是真的泄漏 | GC日志、GC频率、老年代增长曲线、Full GC频率 | ★★★★★ |
| 2 | 确定是哪个代区在泄漏 | Old区 / Survivor / Eden / Metaspace / CodeCache / Direct | ★★★★★ |
| 3 | 最快找到最可疑的大对象 | jmap -histo:live / VisualVM / jcmd GC.class_histogram | ★★★★☆ |
| 4 | 找引用链最强的那个 | dump → MAT / jhat / heap dump 分析工具 | ★★★★☆ |
| 5 | 看保留集最大的那一类对象 | MAT → Dominator Tree / Component Report | ★★★★ |
| 6 | 重点看各种集合类 | HashMap / ConcurrentHashMap / ArrayList / LinkedList / WeakHashMap / Guava Cache / Caffeine | ★★★★ |
| 7 | 重点看各种“持有型”对象 | ThreadLocal / Thread / Connection / Session / Listener / Observer / ScheduledExecutorService / Timer | ★★★ |
| 8 | 看有没有堆外内存泄漏 | NMT(Native Memory Tracking)、jcmd、pmap、VisualVM 插件 | ★★☆ |
最高频、最经典的 13 种 Java 内存泄漏场景(按出现频率排序)
| 排名 | 场景 | 典型特征 | 发现方式 | 修复难度 |
|---|---|---|---|---|
| 1 | 各种 Map / List / Set 长期越积越多 | 最最最常见 | dump → 看保留集最大的类 | ★★☆ |
| 2 | 没有设置过期时间的各种缓存 | Guava/Caffeine/Ehcache/HashMap做缓存 | 看缓存对象数量持续上涨 | ★☆☆ |
| 3 | ThreadLocal 用完没有 remove() | 线程池 + ThreadLocal 是地狱组合 | dump → 看 ThreadLocalMap | ★★ |
| 4 | 各种 Listener / Observer 没注销 | 事件总线、GUI、Spring 事件、MQ消费者等 | 看 Listener 集合持续增长 | ★★☆ |
| 5 | 连接池/会话/资源没有 close() | HttpClient、数据库连接、Socket、Stream等 | 看连接对象持续增长 | ★★ |
| 6 | 单例持有大量临时对象 | 静态 Map / List / 各种 Factory 持有 | 看静态字段引用的对象特别多 | ★★☆ |
| 7 | 线程没结束(死循环/阻塞) | 自定义线程、线程池任务阻塞 | 看线程数量持续增加 | ★★★ |
| 8 | 大对象一直被强引用 | 全局配置、大的静态 byte[]/char[] | 看 Dominator Tree 前几名 | ★★ |
| 9 | Netty ByteBuf 没有 release | Netty 程序最常见的泄漏点 | 看 DirectByteBuffer 疯狂增长 | ★★★ |
| 10 | 类加载器泄漏(热部署/插件系统) | spring-boot-devtools / osgi / 自定义类加载器 | 看 Metaspace 持续上涨 | ★★★★ |
| 11 | Unsafe / DirectByteBuffer 滥用 | 自己手动分配了又不释放 | 用 NMT 查看 external/native | ★★★★ |
| 12 | WeakHashMap 做缓存却把 key 强引用 | 非常经典的错误用法 | 看 WeakHashMap 没被回收 | ★★★ |
| 13 | Lambda/匿名内部类 引用外部对象 | 比较隐蔽,但会把外部对象拉长生命周期 | 看 $ 匿名类引用链 | ★★★☆ |
快速定位口诀(背下来非常有用)
老年代一直涨 → 看大对象 → 看集合 → 看缓存 → 看 ThreadLocal
线程池 + ThreadLocal → 十有八九是 ThreadLocal 没 remove
Netty + 堆外疯涨 → 99% 是 ByteBuf 没 release
Metaspace 一直涨 → 怀疑热部署/类加载器/大量动态代理/CGLIB
GC 日志里 Full GC 非常频繁且回收很少 → 基本可以断定是泄漏
目前最高效的整套工具链组合(2024-2025主流)
| 顺序 | 工具 | 主要用途 | 速度 | 推荐指数 |
|---|---|---|---|---|
| 1 | GC日志 + GC easy | 快速判断是不是泄漏、哪个代区 | ★★★★★ | ★★★★★ |
| 2 | jcmd / jmap -histo | 快速看活着的对象分布 | ★★★★ | ★★★★☆ |
| 3 | jstack + arthas | 看线程、看锁、看阻塞 | ★★★★ | ★★★★ |
| 4 | Heap Dump + Eclipse MAT | 最强引用链分析 | ★★★ | ★★★★★ |
| 5 | VisualVM + 插件 | 综合监控、堆外、类、线程 | ★★★★ | ★★★★☆ |
| 6 | jcmd GC.run/finalizer | 强制执行 finalizer 看效果 | ★★ | ★★★ |
| 7 | NMT + jcmd VM.native_memory | 堆外内存泄漏专用 | ★★ | ★★★★(堆外必备) |
最后总结 —— 最高效的排查思维导图(建议保存)
发现内存泄漏
├── 看 GC日志 → 老年代持续上涨? → 是 → 进入堆内排查
│ │
│ └─ 不是 → 看是不是堆外 / Metaspace / CodeCache
│
└── 堆内泄漏
├── 优先 dump → MAT
│ ├── 看 Dominator Tree 前10
│ ├── 看 Leak Suspects
│ ├── 看 Component Report(集合类)
│ └── 看 ThreadLocalMap 那一项
│
├── 快速看一遍 live histogram
└── 重点排查下面这几个(顺序)
1. 所有 static Map/List/Set/缓存
2. ThreadLocal
3. 所有 Listener/Callback/Observer
4. 所有连接池/客户端/资源对象
5. 自己写的各种长生命周期大容器
口诀:先GC日志 → 再大对象 → 再集合 → 再 ThreadLocal → 最后堆外
有想针对其中某一步、某种具体场景、某种工具的使用细节深入的,可以继续告诉我~