Metaspace溢出根本原因是类卸载失败而非类数量过多,主因是类加载器泄漏——只要类加载器存活且被强引用,其加载的所有类元数据就持续驻留本地内存直至OOM。

Metaspace 溢出不是因为“类太多”,而是因为“类卸不掉”。只要类加载器还活着,它加载的所有类元数据就一直钉在本地内存里,持续累积直到撑爆空间。
根本原因:类加载器泄漏导致元数据无法回收
Metaspace 存储的是类的元数据(类名、方法签名、注解等),由类加载器生命周期管理。一旦某个类加载器被业务对象强引用(比如单例 Bean 持有 WebAppClassLoader、代理对象缓存了 CGLIB 类加载器),它加载的所有类就永远无法卸载——哪怕这些类早已不用。
- Tomcat 多次 redeploy 后旧 WebAppClassLoader 残留
- Spring Boot DevTools 热重启未清理 BundleClassLoader
- AOP 代理对象被静态/单例容器长期持有,连带其 DelegatingClassLoader 无法回收
- Fastjson/Jackson 反序列化频繁触发反射膨胀,生成大量 GeneratedMethodAccessor\d+ 类,而对应的 Method 对象未被释放
高频诱因:动态类生成失控
不是生成类本身危险,而是生成后没被 GC 回收。常见场景包括:
- CGLIB/ASM 动态代理:如 Spring AOP 使用 proxy-target-class=true 时,每个被增强类都生成 ClassName$$EnhancerByCGLIB$$xxx,若增强器实例被缓存,类加载器就卡住
- JDK 反射膨胀:默认调用 15 次后自动生成 GeneratedMethodAccessor 类;加参数 -Dsun.reflect.inflationThreshold=1 可快速验证是否为瓶颈
- 脚本引擎滥用:Groovy/Janino 每次编译都产生新类,且常绑定到线程上下文类加载器
诊断与定位关键步骤
别急着调大 -XX:MaxMetaspaceSize,先确认是不是泄漏:
立即学习“Java免费学习笔记(深入)”;
- 运行 jstat -gcmetacapacity <pid> 查看 MC(当前容量)、MU(已使用)和 CCSC(压缩类空间)趋势,持续上涨说明卸载失败
- 执行 jmap -clstats <pid>,重点关注 sun.reflect.DelegatingClassLoader 和 org.springframework.cglib.core.internal.Function 加载的类数量,超 200 个即高风险
- 用 Arthas 执行 vmtool --action getInstances --className java.lang.ClassLoader,筛选存活但应已废弃的类加载器实例
- 开启类加载/卸载日志:-XX:+TraceClassLoading -XX:+TraceClassUnloading,观察是否有“loaded”但无对应“unloaded”记录
真正有效的解决方向
治标:临时缓解;治本:切断泄漏源头。
- 限制反射膨胀:启动时加 -Dsun.reflect.noInflation=true,强制所有反射走预编译路径,避免生成 Accessor 类
- 复用动态类:CGLIB 设置 enhancer.setUseCache(true);Hibernate、MyBatis 等框架启用 bytecode provider 缓存
- 清理强引用链:检查 AOP 切面中是否缓存了代理对象;避免在 ThreadLocal 或静态 Map 中持有 ClassLoader 实例
- 合理设置上限:-XX:MetaspaceSize=128m(触发首次 GC 的阈值),-XX:MaxMetaspaceSize=512m(防系统内存耗尽),不建议设过大


















