内存泄漏表现为老年代使用率持续单向上升、full gc频率增加但回收量递减、无业务增长下的堆线性增长;通过jstat监控o/fgc/fgct指标,结合mat分析堆快照中实例数与gc roots引用链,可定位静态集合未清理、threadlocal未remove等根因。

Java程序内存占用“平稳但持续增长”,说明没有剧烈抖动,也未立即触发OOM,但趋势异常——这恰恰是典型内存泄漏的隐蔽表现。关键不在于是否暴涨,而在于增长不可逆、GC无法回落、老年代缓慢爬升。
看准三个核心指标,确认是否真泄漏
- 老年代(Old Gen)使用率持续单向上升:比如每小时上涨0.5%,一天后从40%升到85%,即使多次Full GC也仅小幅回落(如85%→82%→84%→86%),说明对象在老年代堆积且未被回收。
-
Full GC频率增加但回收量递减:观察
jstat -gcutil <pid> 1000 20</pid>输出,若FGC次数增多,但每次FGCT耗时变长、OU(Old Used)下降幅度越来越小(如某次只回收2MB),就是强信号。 - 无业务增长前提下的堆增长:流量、QPS、并发数稳定,但堆内存曲线仍线性上扬,排除了正常负载导致的临时内存占用。
用 jstat 快速抓取异常模式
执行命令:
jstat -gcutil <pid> 5000 12</pid>
(每5秒采样一次,共12次,覆盖1分钟趋势)
重点关注列:
-
O(Old generation usage %):是否单调递增? -
FGC/FGCT:Full GC是否越来越频繁?总耗时是否明显拉长? -
GCT(总GC时间占比):若超过10%,已影响吞吐;超20%需紧急干预。
若发现O从70%→73%→76%→79%→82%……且每次FGC后仅降1~2%,基本可锁定为内存泄漏。
间隔导出堆快照,做横向对比
不要等OOM才dump。在确认增长趋势后,每隔30~60分钟手动导出一次堆快照:
jmap -dump:live,format=b,file=heap_$(date +%H%M).hprof <pid></pid>
得到多个.hprof文件(如 heap_1030.hprof、heap_1100.hprof、heap_1130.hprof)。
用MAT打开后,对比两个关键视图:
-
Histogram → Group by package/class:看哪些类的实例数/保留大小增长最显著(例如
com.example.cache.UserSession实例从500→2000→8000); - Dominator Tree:找“吃内存大户”,再右键 → Path to GC Roots → exclude weak/soft references,看是谁在强引用它(大概率指向某个静态Map、缓存容器或监听器列表)。
直接定位代码常见高危点
-
静态集合没清理:
private static Map<string object> cache = new HashMap();</string>
→ 检查是否有put()但无对应remove(),尤其在异步回调、监听注册后未反注册。 -
ThreadLocal 未 remove():在线程池复用场景下,
ThreadLocal.set()后忘记remove(),导致对象随线程长期滞留。 -
未关闭的资源持有引用:如
ZipInputStream、ResultSet、ByteBuffer等,底层缓冲区可能被间接持住。 -
内部类意外延长外部类生命周期:非静态内部类隐式持有
this引用,若被线程、定时器、事件总线长期持有,整个外部实例无法回收。
补充建议:让排查更高效
- 启动JVM时加参数
-XX:+PrintGCDetails -Xloggc:gc.log,保留GC日志,便于回溯时间点; - 生产环境慎用
jmap -dump(会STW),可改用-XX:+HeapDumpOnOutOfMemoryError+ 定期jcmd <pid> VM.native_memory summary scale=MB</pid>辅助判断; - 若应用支持,接入Arthas:
monitor -c 5 com.example.service.UserService getUser查看方法调用频次与对象生成节奏,辅助关联增长时段。
不复杂但容易忽略——平稳增长比暴增更危险,因为它让你误以为“还能撑”。早一小时确认泄漏,就少十小时排查成本。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











