“老年代越收越满”的最直接日志信号是:多次Full GC后,PSOldGen或G1OldGen等老年代区域的“回收后占用”持续上升且释放量锐减,如6800K→7150K→7620K,总大小不变,回落幅度逐次收窄。

GC日志里怎么看“老年代越收越满”
内存泄漏最直接的日志信号,不是某次GC失败,而是老年代(Old Gen)在多次Full GC后,回收前后的内存占用持续走高、且释放量越来越小。比如连续三次Full GC日志中,ParOldGen或PSOldGen区域的“回收后占用”分别是 6800K → 7150K → 7620K,而总大小始终是 8192K,说明每次只勉强挤出几十KB,几乎不释放——这就是典型泄漏迹象。
关键不是看绝对值,而是看趋势:如果老年代使用率长期 >90% 且每次Full GC后回落幅度
- 别只盯
Heap: X->Y总堆变化,它可能被新生代波动掩盖;必须拆开看PSOldGen或G1OldGen等具体区域 - JDK 8 日志里找
[PSOldGen: A->B(C)];JDK 11+ 统一日志中搜索Old或tenured标签段 - 如果日志里频繁出现
(Allocation Failure)触发 Full GC,但老年代回收后仍接近上限,说明新对象不断晋升且无法清理
为什么“Minor GC频率飙升”也是泄漏前兆
表面看是新生代问题,实则常是泄漏的早期反射。当大量本该短命的对象因强引用滞留(比如被静态Map持有),它们会在几次Minor GC后迅速达到晋升年龄,批量涌向老年代——这会直接导致两个现象:Eden区反复快速填满,Minor GC间隔从分钟级缩到几秒;Survivor区存活对象年龄分布异常集中(靠-XX:+PrintTenuringDistribution确认)。
- 用
jstat -gc <pid>实时验证:若YGCT(Young GC总耗时)和YGCT次数在10分钟内激增3倍以上,且OGCMN/OGCMX(老年代最小/最大容量)没变,大概率有对象在“抢道晋升” - 开启
-XX:+PrintTenuringDistribution后,日志中若反复出现age 1: 123456 bytes占比超80%,说明对象几乎不经历Survivor就直奔老年代 - 这种现象常伴随
ParNew或G1 Evacuation Pause耗时明显增长,因为GC要搬运更多“不该在这儿”的对象
别被“Metaspace稳定”骗了
看到日志里Metaspace: XXXK->XXXK纹丝不动,很多人就排除类加载泄漏——这是个经典误判。Metaspace本身不泄漏,但它的“稳定”可能掩盖了真正的问题:比如自定义类加载器未释放,导致其加载的类实例(如com.example.ServiceImpl)一直被强引用,而这些实例占的是堆内存,不是Metaspace。
- Metaspace大小不变 ≠ 类加载正常;需结合堆转储查
ClassLoader实例数是否持续增长 - 若日志中
FullGC频繁但Metaspace无变化,反而要重点怀疑业务代码里用了new URLClassLoader却没调close() - 用
jmap -clstats <pid>可快速统计类加载器数量,>100个且随时间增加,基本坐实问题
用GCViewer或GCEasy快速验证趋势,但别跳过原始日志
工具能画出“老年代使用率曲线”,但自动结论常漏掉上下文。比如GCViewer标红“High Old Gen Usage”,但你得手动翻日志确认:是不是某次Full GC前刚处理完一个大报表导出任务?那可能是合理峰值,而非泄漏。
- 导入GC日志前,先用
grep "Full GC" gc.log | head -20扫一眼最近20次的回收效果,比看图表更快定位拐点 - GCEasy报告里的“Memory Leak Suspected”只是提示,真正要追的是它标出的“Highest Retained Heap”对应类,在堆转储里用MAT查
Path to GC Roots - 生产环境禁用
-XX:+PrintHeapAtGC(太重),但排查期务必加上——没有GC前后的堆快照,光看数字就是猜
真正难的不是看出“老年代涨了”,而是确认它为什么涨不下去:是缓存没过期?监听器没注销?还是某个线程池里的任务对象被意外持有了三年?日志只告诉你“堵在哪”,得靠堆转储和代码逻辑一起推演。








