老年代使用率持续上涨且Full GC后不回落是内存泄漏最典型信号;需用jstat -gcutil PID 1000实时监控O、M、S0/S1/E指标,结合GC日志、堆转储与MAT分析定位根因。

直接看老年代使用率是否持续上涨、GC 后不回落,这是内存问题最典型的信号。重点不是“有没有 Full GC”,而是“老年代对象为什么清不掉”。
盯住 jstat 实时指标,快速定位压力来源
执行 jstat -gcutil PID 1000,每秒刷新一次,重点关注三项:
- O(Old)使用率:持续 >85% 且每次 Full GC 后下降很少 → 老年代对象堆积,大概率是内存泄漏或对象长期存活
- M(Metaspace)使用率:持续上涨甚至到 100% → 类加载泄漏,常见于热部署、动态代理、OSGi、频繁 new ClassLoader
- S0/S1 和 E(Eden)状态:S0/S1 长期接近满、Eden 极快打满 → 新生代太小或 Survivor 区过小,导致对象提前晋升老年代,间接加剧老年代压力
抓取并分析 GC 日志,确认触发类型
没有日志就等于盲排。若未提前开启,优先用 jinfo -flag +PrintGCDetails PID 尝试动态添加(JDK8+ 支持部分参数),再配合 jstat 观察是否生效。关键日志特征:
- 老年代占满型:日志含 Allocation Failure + tenured generation 或 old → 对象不断晋升,老年代空间不足
- MetaSpace 满型:出现 Metaspace allocation failure → 加载类过多,如 Spring Boot 多次刷新上下文、CGLIB 动态代理爆炸式生成类
- System.gc() 主动触发:明确打印 Full GC (System.gc()) → 检查代码、日志框架(如 Log4j2 的 shutdown hook)、监控 SDK 是否调用
- CMS 并发失败:含 concurrent mode failure → CMS 收集器下老年代碎片化严重或预留空间不足,已逐步淘汰,建议迁移到 G1/ZGC
在合适时机 dump 堆快照,锁定大对象和引用链
别等 OOM 再 dump —— 那时堆已失真。最佳时机是:老年代使用率 75%~90%,且 Full GC 频繁但尚未崩溃。命令推荐:
立即学习“Java免费学习笔记(深入)”;
-
jmap -dump:live,format=b,file=heap.hprof PID(加
live减少业务阻塞,适合生产) - 若进程响应敏感,先摘流量再 dump,避免 STW 影响用户
用 MAT(Eclipse Memory Analyzer)打开后,重点看:
- Leak Suspects Report:自动标出可疑泄漏点,如大量重复的 byte[]、HashMap$Node、自定义 DTO 实例
- Top Consumers:按 retained heap 排序,找占用最大的类 —— 若 int[]、byte[] 占比异常高,说明有缓存膨胀或大文件/JSON 解析未流式处理
- Path to GC Roots:对可疑对象右键 → “Show Objects by Class” → 选实例 → “Merge Shortest Paths to GC Roots” → 查看谁在强引用它(比如 static Map、ThreadLocal、未注销的监听器)
结合代码与中间件,验证高频泄漏场景
不用全盘审计,聚焦四类高危模式:
- 静态集合无清理:static List/Map 缓存订单、会话、配置,没设 size 限制或 LRU 策略
- ThreadLocal 未 remove():线程池中复用线程,每次 set 大对象(如 byte[])却不 remove,Value 永久滞留
- 资源未关闭:数据库连接、Redis 连接、文件流、ZipInputStream 在 finally 块漏了 close(),关联的缓冲区、元数据对象无法释放
-
第三方组件配置不当:Tomcat 默认 request buffer 10MB、Netty PooledByteBufAllocator 未限制最大 chunk、Jackson ObjectMapper 配置了
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES = false导致解析失败后缓存大量临时结构


















