Metaspace OOM 是因类元数据(类名、方法签名等)耗尽本地内存所致,典型现象包括 MU/MC 持续增长、ClassLoader 数量异常、Class 模块内存飙升;定位命令有 jstat -gc、jmap -clstats、jcmd VM.native_memory 等。

Metaspace OOM 的典型现象和定位命令
Java 8+ 出现 java.lang.OutOfMemoryError: Metaspace,不是堆内存问题,而是类元数据(类名、方法签名、常量池、注解等)占满本地内存。JVM 不会自动回收已加载的类,除非对应的 ClassLoader 被 GC 掉——这点和老的永久代(PermGen)行为一致,但底层机制不同。
确认是否真为 Metaspace 问题,优先用以下命令实时观察:
-
jstat -gc <pid>:看MU(Metaspace used)和MC(Metaspace capacity)是否持续增长、逼近MGCMN/MGCMX -
jmap -clstats <pid>:查活跃类加载器数量,若远超预期(如几千个),大概率是动态类生成失控 -
jcmd <pid> VM.native_memory summary scale=MB:确认Class模块内存占用是否异常飙升
动态类生成场景下最容易踩的坑
不是所有反射或代理都会触发 Metaspace OOM,关键在「类定义是否被反复注册进 JVM 元空间」。常见高危组合:
- 使用
CGLIB或ByteBuddy生成代理类,但每次请求都新建Enhancer实例且未复用Class缓存 - Spring AOP 在非单例 Bean 上开启
@Scope("prototype")+@EnableAspectJAutoProxy(proxyTargetClass = true),导致每个 Bean 实例都生成独立子类 - 自定义
ClassLoader加载字节码后不释放引用(比如缓存了Class对象却没清空其ClassLoader引用链) - 热部署工具(如 Spring Boot DevTools、JRebel)在反复重启时残留旧
ClassLoader,而新类仍在不断注入
Metaspace 参数调优与真实约束
-XX:MaxMetaspaceSize 不是越大越好。它限制的是 JVM 向 OS 申请的 native 内存上限,设得过高可能掩盖泄漏,设得太低又会让应用过早崩溃。真正要盯的是泄漏源头,而非参数。
立即学习“Java免费学习笔记(深入)”;
-
-XX:MetaspaceSize:初始触发 GC 的阈值(默认约 20MB),设太小会导致频繁 GC;设太大则延迟发现泄漏 -
-XX:MinMetaspaceFreeRatio和-XX:MaxMetaspaceFreeRatio控制 GC 后空间收缩比例,默认 40/70,调整过大会让 Metaspace 容量“只增不减” - 64 位 Linux 下,每个类平均消耗约 1–3KB 元数据;10 万个类 ≈ 200MB+,别指望靠调参扛住无节制生成
验证是否由动态类引起:一个可落地的检测脚本
不需要改代码,用 JVM 自带工具就能快速验证。在疑似环境执行:
jcmd <pid> VM.class_hierarchy -all | grep "generated" | head -20
如果大量输出含 $$EnhancerByCGLIB$$、$$ByteBuddy$$、$$FastClassBySpringCGLIB$$ 等字样,基本锁定动态类爆炸。再配合 jmap -histo <pid> | grep -i "classloader" 查看不同 ClassLoader 实例数,若某类加载器实例数 > 500,大概率就是它没被回收。
复杂点在于:类加载器是否可达,取决于整个引用链——线程局部变量、静态集合、缓存容器、甚至日志框架的 MDC 都可能意外持有着。这种泄漏不会报错,只会慢慢吃光 Metaspace。










