jvm内存泄漏排查三步法:先用jstat -gcutil 1000盯o/fgc/fgct三列异常确认泄漏;再用jmap -histo:live | head -n 20锁定业务类膨胀对象;最后jmap -dump:live导出快照,mat分析leak suspects与path to gc roots定位static、threadlocal等强引用源头。

直接上手,别绕弯。JVM内存泄漏排查不是理论考试,是线上救火能力——重点在“快定位、准归因、不重启”。按真实生产节奏走,三步就能打出有效组合拳。
先盯住老年代是否真在 leak
jstat 是第一响应工具,命令只要一条:jstat -gcutil <pid> 1000</pid>(每秒刷新)
重点看三列:
- O 列(Old Gen 使用率):持续上升、Full GC 后回落极少(比如从 85% → 82% → 84% → 83%),说明对象“死而不僵”
- FGC 列(Full GC 次数):90 秒内从 0 涨到 10+,但每次回收后 O 值几乎没变
- FGCT 列(Full GC 总耗时):数值明显拉长,同时 YGC 频次也升高,说明新生代对象过早晋升
三项同步异常,基本可断定堆内存在活跃泄漏。
再抓膨胀对象,锁定业务类
确认泄漏后,立刻执行:jmap -histo:live <pid> | head -n 20</pid>
注意加 :live 参数,只统计当前可达对象,排除干扰。重点关注:
- 实例数(#instances)和占用字节(#bytes)双高,且属于你自己的包路径(如
com.yourcompany.cache.UserSessionCache) - 常见可疑数组类型:
[C(大量字符串)、[[I(二维 int 数组,可能是缓存结构)、[Ljava.lang.Object;(Object 数组,集合扩容常见) - 若某类每分钟新增数千实例,而 QPS 没变化,大概率就是泄漏源头。
最后导堆快照,用 MAT 看引用链
执行:jmap -dump:live,format=b,file=leak-$(date +%s).hprof <pid></pid>
加 live 和时间戳,保证快照轻量、可比对。用 MAT 打开后:
- 先跑 Leak Suspects Report,它会自动标出最可疑的几个对象
- 再看 Dominator Tree,找占比最高的对象及其强引用路径
- 关键线索藏在 Path to GC Roots 里:
-
static字段(如静态 Map 缓存未清理) -
ThreadLocal变量(尤其在线程池中未 remove) - 未注销的监听器、回调或事件订阅
- 未关闭的流、连接、Channel(特别是 NIO DirectBuffer)
-
顺手检查几处容易被忽略的配置
- 运行
jinfo -flags <pid></pid>看是否开了-XX:+HeapDumpOnOutOfMemoryError,没开就补上,下次 OOM 自动留证据 - 用
jstat -gcmetacapacity <pid></pid>查元空间使用率,长期 >90% 可能是类加载泄漏 - 开 GC 日志:
-Xlog:gc*:file=gc.log:time,uptime,pid,tags,level,用 GCViewer 或 GCEasy 看 Full GC 是否“白干”
整个过程不需要改代码、不重启服务,5 分钟内完成初步定位。真正卡点不在工具,而在能否把监控趋势、对象分布、引用路径串成一条证据链——O 列涨了 → 哪个类涨得最凶 → 它为什么活下来 → 谁一直拽着它不放。











