MetaspaceSize 与 MaxMetaspaceSize 不等会导致元空间动态扩容引发频繁 Full GC 和 OOM;因 MetaspaceSize 是首次元空间 GC 阈值而非初始大小,过小且 Max 缺失时会触发“扩容→GC→再扩容”震荡循环,持续逼近内存上限。

MetaspaceSize 和 MaxMetaspaceSize 必须设为相等值,否则默认的动态扩容机制会在运行时反复触发 Full GC,且极易因类加载器泄漏或代理类爆炸而突然 OOM。
为什么 MetaspaceSize 不等于 MaxMetaspaceSize 就会出问题
MetaspaceSize 不是“初始大小”,而是 JVM 决定是否触发第一次 Metaspace Full GC 的阈值。它默认仅 21MB,远低于实际应用类加载量。一旦达到该值,JVM 就执行一次 Full GC 并尝试卸载类;若卸载效果差(比如类加载器没被回收),GC 后会自动上调 MetaspaceSize,下次再触发……形成“扩容 → GC → 再扩容 → 再 GC”的震荡循环。
这个过程不仅带来 STW 停顿,更关键的是:每次上调都让元空间占用更接近系统内存上限,而 MaxMetaspaceSize 若未显式设置或设得过大,最终可能耗尽本地内存,报出 java.lang.OutOfMemoryError: Metaspace。
- 现象:应用启动后几分钟内出现多轮
Full GC (Metadata GC Threshold)日志 - 根本原因:MetaspaceSize 过小 + MaxMetaspaceSize 缺失/过大,导致失控增长
- 修复逻辑:用固定上限代替试探性扩容,把不确定性转为可预期容量
怎么设 MetaspaceSize 和 MaxMetaspaceSize 的具体值
不能拍脑袋填 512m 或 1g。真实值必须基于你应用实际加载的类数量和动态行为来确定。最稳妥的做法是压测 + 监控 + 观察稳定水位。
- 先在测试环境用
jstat -gc <pid></pid>持续观察MU(Metaspace used)峰值,取压测后 95% 分位值再上浮 20% - 轻量 Spring Boot 应用(无 CGLIB、无 Groovy、无热重载):128m~256m 足够
- 含 Feign/Hibernate/MyBatis 代理、或使用 Spring AOP 的服务:建议从 384m 起步,压测后确认
- 使用 DevTools、OSGi、或运行大量脚本语言(如 Groovy 模板):至少 512m,动态生成密集型需设到 1g
- 务必让
-XX:MetaspaceSize=256m和-XX:MaxMetaspaceSize=256m两个参数值完全一致
类加载器泄漏才是元空间持续增长的真正推手
即使设置了合理上限,只要类加载器没被回收,它加载的所有类元数据就永远驻留元空间——MaxMetaspaceSize 只是最后一道防线,不是根治手段。
- 典型泄漏场景:
spring.devtools.restart.enabled=true(DevTools 热重载)、Tomcat 频繁 reload WebApp、自定义 ClassLoader 持有静态引用 - 验证方式:用
jmap -clstats <pid></pid>查看类加载器实例数是否随时间递增 - 关键检查点:每个类加载器是否被 ThreadLocal、静态集合、缓存等强引用住?是否调用了
ClassLoader.close()? - 生产环境必须禁用 DevTools:
spring.devtools.restart.enabled=false,否则元空间迟早爆
监控与告警必须覆盖元空间的真实使用率
只看 Usage.used 绝对不够。元空间没有“已用 / 总容量”这种简单比例,它的“总容量”(Usage.max)在未设 MaxMetaspaceSize 时是 -1,监控图表会直接失效。
- 正确指标组合:
Usage.used(当前已用)、Usage.committed(已向 OS 申请的内存)、Non-heap memory usage(JMX 中java.lang:type=MemoryPool,name=Metaspace) - 必须开启 GC 日志:Java 10+ 用
-Xlog:gc*,metaspace*=trace;旧版本用-XX:+PrintGCDetails -XX:+PrintGCTimeStamps - 告警阈值建议设为
Usage.used / Usage.committed > 0.85,而不是 “used > 500m” 这种绝对值 - 注意:
jstat -gc输出的MC(Metaspace Capacity)是当前已提交容量,不是最大值;MU才是真实占用
元空间溢出从来不是单一参数配置问题,而是类加载生命周期管理、JVM 参数设定、监控覆盖三者脱节的结果。最容易被忽略的点是:你设了 MaxMetaspaceSize,却没关掉 DevTools,也没查过 jmap -clstats —— 此时参数只是延缓了 OOM 时间,而非阻止它发生。

















