Metaspace OOM 不是堆内存问题,因为Metaspace使用本地内存、不受-Xmx控制,其OOM主因是动态代理类等元数据堆积且ClassLoader未卸载。

Metaspace OOM 为什么不是堆内存问题
Java 8+ 把永久代(PermGen)换成了 Metaspace,它默认用本地内存(native memory),不归 -Xmx 管。所以你加了 -Xmx4g 却还是报 OutOfMemoryError: Metaspace,不是 JVM 内存配少了,而是类元数据——尤其是动态生成的类——把 Metaspace 填满了。
典型场景:用 cglib 或 javassist 做 AOP、大量使用 Proxy.newProxyInstance、Spring Boot 启动时反复刷新上下文(比如测试中频繁 new AnnotationConfigApplicationContext())。
关键点:java.lang.Class 对象本身在堆里,但它的结构、方法签名、常量池、注解信息等元数据存在 Metaspace;每生成一个代理类,就多一份元数据,且不会被常规 GC 回收。
怎么确认真是动态代理类撑爆了 Metaspace
别猜,先看证据。加两个 JVM 参数启动应用:
立即学习“Java免费学习笔记(深入)”;
-
-XX:+PrintGCDetails—— 能看到 Metaspace 的 GC 日志(注意:Metaspace GC 只在触发 full GC 或显式System.gc()时才可能回收无用类) -
-XX:+TraceClassLoading和-XX:+TraceClassUnloading—— 实时观察类加载/卸载行为,重点盯住com.sun.proxy.$Proxy、net.sf.cglib.proxy.$EnhancerBySpringCGLIB$这类命名
如果日志里反复出现类似:
Loaded [com.sun.proxy.$Proxy123] fromLoaded [com.sun.proxy.$Proxy124] from ...
而 Unloaded 行极少或没有,基本就是代理类堆积。还可以用 jstat -gc 查看 MU(Metaspace used)和 MC(Metaspace capacity)持续上涨,且 MGCC(Metaspace GC count)几乎为 0。
限制动态代理类数量的实操手段
根本思路不是“加大 Metaspace”,而是让代理类复用、延迟生成、或及时卸载。
- Spring 用户:避免在循环里手动创建
ProxyFactoryBean或反复调用Proxy.newProxyInstance;改用单例 Bean + 接口注入,让 Spring 管理代理生命周期 - cglib 用户:设置
Enhancer.setUseCache(true)(默认 true,但某些自定义ClassLoader下会失效);禁用setDynamicLoading(false)防止每次生成新类名 - 所有用户:检查是否用了匿名内部类实现接口后又去代理它——这会导致每个实例都生成独立代理类;改用命名类 + 显式
Class引用 - 测试场景:用
@DirtiesContext替代手动 new ApplicationContext,确保上下文销毁时关联的代理类能被 ClassLoader 卸载
临时缓解可加 -XX:MaxMetaspaceSize=256m(别设太大,否则掩盖问题),并配 -XX:MetaspaceSize=128m 让 GC 更早介入。
ClassLoader 泄漏比代理类本身更难排查
代理类卸载的前提是它所属的 ClassLoader 被回收。而很多框架(尤其老版本 Spring、Tomcat、OSGi)会持有对动态类加载器的强引用,导致整个加载器连带所有代理类“钉”在内存里。
常见泄漏点:
- 线程局部变量(
ThreadLocal)没清理 - 静态集合缓存了
Class或ClassLoader实例 - Servlet 容器热部署时旧
WebAppClassLoader没被 GC,但新请求又生成一批代理类
用 jcmd 查看加载器树,再用 jmap -clstats 统计各加载器加载的类数;如果某个 sun.misc.Launcher$AppClassLoader 或 org.springframework.boot.loader.LaunchedURLClassLoader 加载了几千个 $Proxy 类,基本就是泄漏了。
这类问题没法靠改一行代码解决,得结合 MAT(Eclipse Memory Analyzer)看 GC Roots,定位谁在持有着那个 ClassLoader。










