【Java】JVM 内存上升了,该如何排查?
🔍 核心排查思路:从宏观到微观
整个排查过程应遵循“先整体,后局部”的原则,避免一开始就陷入代码细节。其科学严谨的排查流程可总结为下图:

🚨 第零步:确立现象 — 是正常上升还是内存泄漏?
在开始复杂排查前,先回答:“内存使用率是否在持续增长且不被GC有效回收?”
专业举证与工具:
-
启用基础监控
- JVM 自带工具:使用
jstat命令观察垃圾回收情况,这是最直接的手段。 - 命令示例:
jstat -gcutil <pid> 2s(每2秒输出一次) - 关键指标解读:
EU(Eden区使用率): 频繁增长后下降,说明年轻代GC正常。OU(老年代使用率): 这是重点!如果这个值持续稳步上升,即使在Full GC后也看不到明显下降,或者下降不多,那么极有可能存在内存泄漏。FGC/FGCT(Full GC次数/时间): 如果FGC越来越频繁,但OU下降不明显,进一步证实内存泄漏。
- JVM 自带工具:使用
-
系统级监控
- 使用
top(Linux) 或Process Explorer(Windows) 观察进程的RES(常驻内存) 和%MEM变化趋势。如果曲线呈“楼梯式”上升(增长后不掉落),则是典型迹象。
- 使用
🔎 第一步:定位问题区域 — 堆内还是堆外?
JVM 内存不止堆内存(Heap),还有堆外内存(Off-Heap)。必须明确问题所在。
1. 堆内内存(Heap)
包括 Young Gen (Eden, S0, S1) 和 Old Gen。大部分内存泄漏发生在这里。
2. 堆外内存(Off-Heap)
- Metaspace: 存储类元数据。如果项目动态生成类(如CGLib代理、Groovy脚本),会导致此区域增长。
- Code Cache: JIT编译后的本地代码。
- 直接内存(Direct Buffer): 通过
ByteBuffer.allocateDirect()分配,常见于NIO、网络通信、序列化框架(如Netty)。 - 线程栈(Thread Stack): 线程过多,每个线程都会占用栈内存。
专业举证与工具:
-
使用 NMT (Native Memory Tracking): 这是诊断堆外内存的利器。
- 启动参数:
-XX:NativeMemoryTracking=detail - 查看命令:
jcmd <pid> VM.native_memory detail - 对比差异: 在内存上升前后分别执行
jcmd <pid> VM.native_memory detail,然后使用jcmd <pid> VM.native_memory summary.diff查看变化,可以清晰看到是哪个区域在增长。
- 启动参数:
-
观察GC日志: 开启GC日志,查看Old区容量变化。
- 启动参数:
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/path/to/gc.log
- 启动参数:
🕵️ 第二步:深入分析 — 获取并分析内存快照
如果确定是堆内内存问题,生成和分析堆转储(Heap Dump)是决定性的一步。
1. 生成堆转储(Heap Dump)
-
使用 jmap 命令(在线转储):
jmap -dump:live,format=b,file=heapdump.hprof <pid>- 注意:
live选项会触发一次Full GC,只转储存活对象。这能更精确地看到泄漏对象,但会暂停应用。在生产环境使用需谨慎。
-
使用 JVM 参数(自动转储,适合OOM时):
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump/heapdump.hprof- 这是最安全、最常用的方式,当OOM发生时自动生成快照,不影响实时服务。
2. 分析堆转储(Heap Dump)
专业工具:Eclipse Memory Analyzer Tool (MAT) 是行业标准。
分析步骤:
- 打开
.hprof文件: MAT会自动生成泄漏嫌疑报告(Leak Suspects Report)。 - 查看“Leak Suspects”: MAT会智能地列出可能导致泄漏的对象和其引用链。这是首要关注点。
- 使用“Histogram”: 查看类的实例数(Objects)和浅堆(Shallow Heap)、保留堆(Retained Heap)大小。
- 保留堆(Retained Heap): 这个指标至关重要!它表示一个对象被GC后能释放的总内存。按 Retained Heap 排序,排在最前面的类就是最大的怀疑对象。
- 举证: 你可能会发现某个自定义类(如
UserSession)有上万个实例,或者一个char[]/String占用了巨量内存。
- 使用“Dominator Tree”: 这是最强大的功能。它列出内存中最大的“对象支配树”,能清晰地展示出哪些对象直接或间接持有了大量内存。
- 定位代码: 在 Dominator Tree 或 Histogram 中右键点击可疑对象,选择 “Merge Shortest Paths to GC Roots” -> “exclude all phantom/weak/soft etc. references”。这会显示从该对象到GC Roots(如线程栈、静态变量)的完整引用链。这条链就是阻止对象被回收的“罪魁祸首”!
案例举证:
- 场景: 一个Web应用,内存持续增长。
- MAT分析结果:
- 最大对象:
java.util.HashMap$Node数组,Retained Heap 为 1.2GB。 - 路径到GC Roots:
Tomcat ThreadPool Executor->WorkerThread->ThreadLocalMap->MyApplicationClass->static ConcurrentHashMap->HashMap$Node[]。 - 结论: 应用代码在
ThreadLocal中使用了静态Map,但线程池中的线程复用后没有清理ThreadLocal,导致每个线程的ThreadLocal数据都堆积在静态Map中无法释放。这就是典型的内存泄漏。
- 最大对象:
🧩 第三步:针对特定场景的排查
1. 排查堆外内存
- 如果NMT显示是
Internal或Arena增长,很可能与JNI代码或Netty等有关。Netty 4.1+ 提供了更精细的内存泄漏检测机制-Dio.netty.leakDetectionLevel=paranoid。 - 使用
jstack检查线程数:jstack <pid> | grep ‘java.lang.Thread.State’ | wc -l。如果线程数异常多,可能是线程池配置不当导致线程泄漏。
2. 排查 Metaspace
- 检查是否有大量动态类生成。在MAT中,可以查看
Class对象的实例(是的,类本身也是对象)。如果某些自定义类加载器加载的类数量异常多,且无法被卸载,就会导致Metaspace泄漏。
🛠️ 第四步:修复与验证
- 修复代码: 根据MAT分析出的引用链,修复代码中的问题。常见原因包括:
- 未关闭的资源(数据库连接、文件流、Sockets)。
- 静态集合类滥用,无清理机制。
- 监听器或回调未正确注销。
ThreadLocal使用后未清理。
- 验证修复: 在测试环境使用相同的监控和压测手段,观察内存曲线是否变得平稳(锯齿状),OU是否能在Full GC后有效回落。
💎 总结:专业排查 checklist
- [ ] 使用
jstat确认老年代使用率(OU)趋势。 - [ ] 使用
jcmd和 NMT 区分堆内/堆外内存问题。 - [ ] 使用
-XX:+HeapDumpOnOutOfMemoryError或jmap获取堆转储。 - [ ] 使用 MAT 加载堆转储,按 Retained Heap 排序,找到最大的对象。
- [ ] 使用 “Path to GC Roots” 功能找到阻止回收的引用链。
- [ ] 结合代码,修复引用链上的问题点(如清理静态集合、关闭资源)。
- [ ] 在预发布环境进行压测,对比修复前后的内存监控图。
这份指南涵盖了从现象确认到根因定位的完整专业流程。熟练掌握这些工具和思路,你就能应对绝大多数JVM内存上升问题。
本文来自博客园,作者:NeoLshu,转载请注明原文链接:https://www.cnblogs.com/neolshu/p/19513694

浙公网安备 33010602011771号