Full GC后老年代内存未明显下降,说明回收效果差,主因是对象长期存活、强引用未断或元空间/直接内存耗尽;需结合GC日志中Promotion Failed或Metadata GC Threshold等线索定位根因。

Full GC 后老年代内存没怎么降,说明回收效果差——不是 GC 没跑,而是该回收的对象根本没被回收。这大概率是对象长期存活、强引用未断,或者元空间/直接内存等非堆区域耗尽导致的“假性堆高”。得拆开看,不能只盯着 Old Gen 数值。
看 GC 日志里有没有 Promotion Failed 或 Metadata GC Threshold
这两类日志是关键线索,直接指向不同根因:
-
Promotion Failed:表示 Minor GC 时 Survivor 放不下存活对象,又没法晋升到老年代(老年代空间不足或碎片化),被迫触发 Full GC。此时即使 Full GC 完了,老年代可能还是满的——因为根本没地方收。 -
Metadata GC Threshold或Last ditch collection:说明元空间快爆了,JVM 在做垂死挣扎式的元数据 GC。这种 Full GC 不动堆内存,O(Old)列几乎不变,但M(Metaspace)列会逼近 100%。 - 如果日志里出现
java.lang.OutOfMemoryError: Metaspace,那堆内存再低也救不了——进程会因元空间 OOM 直接退出。
用 jstat -gc 看各区域真实水位
别只信监控图上的“堆使用率”,它常把元空间、直接内存、线程栈全算进“进程内存”,误导判断。真正要盯的是:
-
O列(Old Gen 使用率):持续 >85% 且 Full GC 后降幅 -
M列(Metaspace 使用率):>90% 就危险,尤其当XX:MaxMetaspaceSize设得小(比如默认 256M); -
CCS列(Compressed Class Space):JDK8u40+ 引入,和 Metaspace 绑定,过高也预示类加载异常; - 对比
YGC和FGC次数:如果 FGC 频次远高于 YGC(比如 10:1),说明问题不在新生代分配节奏,而在老年代或元空间。
抓 Heap Dump 前先确认是不是 System.gc() 干的
有些框架或 SDK 会悄悄调 System.gc(),日志里显示 Full GC (System)。这种 GC 是“建议式”的,不保证回收效果,还可能干扰 JVM 自适应策略。排查方法:
- 加 JVM 参数
-XX:+DisableExplicitGC临时屏蔽,观察 Full GC 是否消失; - 用
jstack抓线程栈,搜System.gc或Runtime.getRuntime().gc()调用点; - 检查依赖库(如某些旧版 Druid、Log4j 1.x、自定义监控 agent)是否内置了显式 GC 逻辑。
dump 之后重点查三类对象
Heap Dump 不是用来“看谁占得多”,而是看“谁不该活这么久”。打开 MAT 或 JVisualVM 后,直奔:
- 按
dominator_tree排序,找Shallow Heap大但Retained Heap更大的对象——说明它持有一长串引用链; - 过滤
java.util.HashMap、java.util.ArrayList、org.springframework.cache相关类,静态缓存没设过期、监听器没注销、ThreadLocal 没remove()都爱在这藏; - 检查
ClassLoader实例数:如果数量随部署次数增长(比如热更新、OSGi 场景),大概率是类加载器泄漏,最终拖垮元空间。
真正难处理的从来不是“内存高”,而是“为什么 GC 不认这个对象”。堆 dump 里每个存活对象背后都有引用路径,而路径起点往往在业务代码最不经意的 static 字段或回调注册里。别跳过“查看支配者”这一步,也别急着调 -Xmx——多数时候,你只是给泄漏腾出了更大房间。


















