文章中如果有图看不到,可以点这里去 csdn 看看。从那边导过来的,文章太多,没法一篇篇修改好。

【Java】JVM 内存上升了,该如何排查?

🔍 核心排查思路:从宏观到微观

整个排查过程应遵循“先整体,后局部”的原则,避免一开始就陷入代码细节。其科学严谨的排查流程可总结为下图:

在这里插入图片描述


🚨 第零步:确立现象 — 是正常上升还是内存泄漏?

在开始复杂排查前,先回答:“内存使用率是否在持续增长且不被GC有效回收?”

专业举证与工具:

  1. 启用基础监控

    • JVM 自带工具:使用 jstat 命令观察垃圾回收情况,这是最直接的手段。
    • 命令示例jstat -gcutil <pid> 2s (每2秒输出一次)
    • 关键指标解读
      • EU (Eden区使用率): 频繁增长后下降,说明年轻代GC正常。
      • OU (老年代使用率): 这是重点!如果这个值持续稳步上升,即使在Full GC后也看不到明显下降,或者下降不多,那么极有可能存在内存泄漏。
      • FGC/FGCT (Full GC次数/时间): 如果FGC越来越频繁,但OU下降不明显,进一步证实内存泄漏。
  2. 系统级监控

    • 使用 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) 是行业标准。

分析步骤:

  1. 打开 .hprof 文件: MAT会自动生成泄漏嫌疑报告(Leak Suspects Report)。
  2. 查看“Leak Suspects”: MAT会智能地列出可能导致泄漏的对象和其引用链。这是首要关注点。
  3. 使用“Histogram”: 查看类的实例数(Objects)和浅堆(Shallow Heap)、保留堆(Retained Heap)大小。
    • 保留堆(Retained Heap): 这个指标至关重要!它表示一个对象被GC后能释放的总内存。按 Retained Heap 排序,排在最前面的类就是最大的怀疑对象。
    • 举证: 你可能会发现某个自定义类(如 UserSession)有上万个实例,或者一个 char[]/String 占用了巨量内存。
  4. 使用“Dominator Tree”: 这是最强大的功能。它列出内存中最大的“对象支配树”,能清晰地展示出哪些对象直接或间接持有了大量内存。
  5. 定位代码: 在 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 RootsTomcat ThreadPool Executor -> WorkerThread -> ThreadLocalMap -> MyApplicationClass -> static ConcurrentHashMap -> HashMap$Node[]
    • 结论: 应用代码在 ThreadLocal 中使用了静态Map,但线程池中的线程复用后没有清理 ThreadLocal,导致每个线程的 ThreadLocal 数据都堆积在静态Map中无法释放。这就是典型的内存泄漏。

🧩 第三步:针对特定场景的排查

1. 排查堆外内存

  • 如果NMT显示是 InternalArena 增长,很可能与JNI代码或Netty等有关。Netty 4.1+ 提供了更精细的内存泄漏检测机制 -Dio.netty.leakDetectionLevel=paranoid
  • 使用 jstack 检查线程数jstack <pid> | grep ‘java.lang.Thread.State’ | wc -l。如果线程数异常多,可能是线程池配置不当导致线程泄漏。

2. 排查 Metaspace

  • 检查是否有大量动态类生成。在MAT中,可以查看 Class 对象的实例(是的,类本身也是对象)。如果某些自定义类加载器加载的类数量异常多,且无法被卸载,就会导致Metaspace泄漏。

🛠️ 第四步:修复与验证

  1. 修复代码: 根据MAT分析出的引用链,修复代码中的问题。常见原因包括:
    • 未关闭的资源(数据库连接、文件流、Sockets)。
    • 静态集合类滥用,无清理机制。
    • 监听器或回调未正确注销。
    • ThreadLocal 使用后未清理。
  2. 验证修复: 在测试环境使用相同的监控和压测手段,观察内存曲线是否变得平稳(锯齿状),OU是否能在Full GC后有效回落。

💎 总结:专业排查 checklist

  1. [ ] 使用 jstat 确认老年代使用率(OU)趋势。
  2. [ ] 使用 jcmd 和 NMT 区分堆内/堆外内存问题。
  3. [ ] 使用 -XX:+HeapDumpOnOutOfMemoryErrorjmap 获取堆转储。
  4. [ ] 使用 MAT 加载堆转储,按 Retained Heap 排序,找到最大的对象。
  5. [ ] 使用 “Path to GC Roots” 功能找到阻止回收的引用链。
  6. [ ] 结合代码,修复引用链上的问题点(如清理静态集合、关闭资源)。
  7. [ ] 在预发布环境进行压测,对比修复前后的内存监控图。

这份指南涵盖了从现象确认到根因定位的完整专业流程。熟练掌握这些工具和思路,你就能应对绝大多数JVM内存上升问题。

posted @ 2025-09-30 14:27  NeoLshu  阅读(0)  评论(0)    收藏  举报  来源