Metadata GC Threshold反复触发表明Metaspace持续增长且类卸载失效,典型原因为动态代理(如CGLIB、Spring AOP)导致类加载器泄漏;需结合-XX:+PrintClassLoaderStatistics、jstat和GC日志中Metaspace回收前后数值及unloaded classes为0等指标交叉验证。

看到 GC 日志里反复出现 Metadata GC Threshold,基本可以断定 Metaspace 正在被持续“喂饱”——不是配置小了,而是有东西在不停生成类却无法卸载。动态代理(CGLIB、JDK Proxy、Spring AOP)正是最典型的元凶。
盯紧 Metaspace 回收效果:看数字是否“假回收”
在每次触发 Metadata GC Threshold 的 Full GC 日志行中,重点提取类似这样的字段:
- Metaspace: 10240K->10240K(1056768K):箭头后数值几乎不变,说明这次 GC 对 Metaspace 几乎没清理效果
- 对比前后 MU(Metaspace Used) 和 MC(Metaspace Capacity),若 MU 持续贴近 MC 且每次 GC 后仅下降个位数 KB,就是强信号
- 特别注意 Classes unloaded: 0 或长期为个位数(如 1~3),说明 JVM 根本没成功卸载任何类
结合类加载器统计确认泄漏源头
单靠 Metaspace 行不够,必须启用 -XX:+PrintClassLoaderStatistics 参数。它会在每次 GC 前后打印:
- 已加载类总数(classes)是否持续上涨(比如从 25000 → 28000 → 32000)
- 类加载器数量(class loaders)是否稳步增加,尤其关注非
sun.misc.Launcher$AppClassLoader的加载器(如org.springframework.boot.devtools.restart.classloader.RestartClassLoader或自定义URLClassLoader) - 如果 unloaded classes 长期为 0,而 class loaders 却在涨,大概率是动态代理反复创建新加载器,旧的又因强引用无法回收
交叉验证:用 jstat 快速抓趋势
不必等 OOM,运行中就能发现苗头:
-
jstat -gc <pid>:观察 MU(已用 Metaspace)和 MC(当前容量)是否同步阶梯式上升 -
jstat -gccapacity <pid>:看 MGCMN/MGCMX(Metaspace 初始/最大值)是否已被突破,或 MGCT(Metaspace GC 次数)异常高频 - 搭配
jcmd <pid> VM.metaspace,直接输出当前加载类数、占用空间、最大阈值,比日志更直观
排除 GC 策略干扰:确认类卸载功能真正生效
即使加了 -XX:+ClassUnloading,也可能白配:
- Parallel GC 完全不支持类卸载,配了也无效;CMS 需额外加
-XX:+CMSClassUnloadingEnabled - G1 在 JDK 8u40+ 才默认支持,旧版本需同时加
-XX:+UnlockExperimentalVMOptions - 检查 GC 日志开头是否有
Using G1或Using CMS,再核对是否匹配对应卸载参数,避免“以为开了,其实没用”

















