元空间触发Full GC的根本原因是类加载器泄漏而非类数量过多;表现为Metadata GC Threshold触发、Classes unloaded: 0持续出现、重启后Metaspace占用不归零,本质是类加载器被静态引用/ThreadLocal/未关闭资源强持有而无法回收。

看到 GC 日志里出现 Metadata GC Threshold 或 Metaspace 相关触发字样,基本可以锁定问题不在堆内存,而在类加载器生命周期失控——这是类加载器级联泄漏最典型的日志指纹。
盯住三处关键日志特征
不是泛泛看“Metaspace”这个词,而是聚焦以下三个信号点:
- Full GC 触发原因写明 Metadata GC Threshold:说明 JVM 主动因元空间不足触发了 Full GC,但此时堆内存使用率往往很低(比如老年代才占 20%),这反常现象直接排除堆泄漏可能
- GC 日志中持续出现 “Classes unloaded: 0” 或卸载数量远小于加载数量:例如一行显示 “Classes loaded: 12489, Classes unloaded: 0”,连续多次都为 0,说明类根本没被卸载
- 重启后元空间初始占用不归零,甚至比上次更高:比如第一次启动后 Metaspace 使用 45MB,重启后变成 68MB,再启变成 92MB——这说明类加载器实例未被回收,其加载的 Class 对象连带元数据全部滞留
结合线程上下文类加载器快速定位泄漏源头
类加载器泄漏极少是应用类加载器自身出问题,多由它被意外强引用导致无法回收。重点排查:
- 当前线程是否调用过
Thread.currentThread().setContextClassLoader(),且未在 finally 块中恢复原 loader - 第三方框架(如 Spring Boot、MyBatis、某些 RPC 客户端)是否在初始化时将当前线程上下文类加载器绑定到静态缓存或单例对象上
- 自定义类加载器是否被 ThreadLocal、静态集合、监听器、定时任务等长期持有引用
用 jstat 快速验证类加载器是否堆积
不用等 OOM,现场执行命令就能佐证:
-
jstat -class <pid></pid>:观察 loaded 列持续上涨、unloaded 列几乎不动,且 time(类加载耗时)也缓慢增长 -
jstat -gc <pid></pid>:确认 YGC/Mixed GC 频率正常,但 FGC 频次明显升高,且每次 FGC 后 Metaspace 已用空间下降极少(比如只降 1–2MB)
面试中一句话点透本质
“元空间触发 Full GC 不是因为类太多,而是因为加载这些类的类加载器还活着——只要一个类加载器被任意静态引用、ThreadLocal 或未关闭资源持有着,它加载的所有类就全卡在元空间里,永远卸不掉。”

















