StackTraces
Java已验证

Java OutOfMemoryError: Metaspace 排查与修复

@debug_master32 天前更新阅读 6 min00

Metaspace 是 Java 8 起存放类元数据的地方,走的是本地内存而非堆。报 OutOfMemoryError: Metaspace,多半不是对象太多,而是动态生成的类太多,或者某个 ClassLoader 泄漏导致旧类卸载不掉。先查是不是 -XX:MaxMetaspaceSize 设太小,再查类加载器泄漏。

Issue & Env (案发现场与环境)

Java 8 之前,类的元数据放在永久代(PermGen),大小由 -XX:MaxPermSize 定死。PermGen 大小难预估,设小了报 OutOfMemoryError: PermGen space,设大了浪费堆空间。

Java 8 引入 Metaspace 替换 PermGen。类的名称、字段、方法、字节码、常量池、注解等信息,现在都落到本地内存的一个区域里。默认它没有硬上限,会自动扩展,直到操作系统没内存可给。

受影响版本区间和是否强设上限有关:

  • 没设 -XX:MaxMetaspaceSize 时,Metaspace 默认持续扩张,直到耗尽系统内存。
  • 显式设了小值(比如 128m),动态类一多就撞顶。

容易触发的场景:大量用 CGLIB 或 ASM 动态生成类、反射和动态代理泛滥、web 应用反复热部署但旧 ClassLoader 没释放。

StackTrace(堆栈日志)

应用跑着跑着崩掉,报错落在类定义这一环:

java.lang.OutOfMemoryError: Metaspace
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:1017)
    at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:174)
    at java.net.URLClassLoader.defineClass(URLClassLoader.java:1096)
    at java.net.URLClassLoader.access$100(URLClassLoader.java:101)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:683)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:682)
    at java.security.AccessController.doPrivileged(Native Method)

线上案例里,catalina.out 也这么出现过:

2014-09-25 14:56:32,907 localhost-startStop-1 ERROR ...
java.lang.OutOfMemoryError: Metaspace
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(Unknown Source)
	at java.security.SecureClassLoader.defineClass(Unknown Source)
	at org.apache.catalina.loader.WebappClassLoader.findClassInternal(WebappClassLoader.java:2957)

注意堆栈落在 defineClass 上。这说明是「正在加载新类」时空间不够,跟堆里对象太多是两码事。

Reproduce(复现代码)

要稳定复现,给一个小上限,再用 CGLIB 循环生成类即可。这里用 -XX:MaxMetaspaceSize=32m 把门卡死:

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;

public class MetaspaceLeak {
    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(Object.class);
            enhancer.setUseCache(false);
            enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) -> proxy.invokeSuper(obj, args1));
            enhancer.create(); // 每次生成一个新类,塞进 Metaspace
        }
    }
}

运行:

java -XX:MaxMetaspaceSize=32m -cp .:cglib.jar MetaspaceLeak

过不了多久就抛 OutOfMemoryError: Metaspace。

Root Cause(根因分析)

Metaspace 的用量取决于两件事:加载了多少类、每个类的元数据有多大。所以报错的主因要么是类太多、要么是类太大。

更深一层是「类卸载」失灵。Metaspace 里的类元数据能不能回收,取决于把它加载进来的 ClassLoader 有没有被回收。ClassLoader 之间是父子的引用关系,只要还有引用指向某个 loader,它加载的所有类元数据都别想卸载。

web 应用热部署是重灾区。每次重新部署,容器用一个新 ClassLoader 加载同样的类,旧 loader 本该被丢弃。但只要有线程、静态字段或框架缓存还拽着旧 loader 的一条引用,旧的类元数据就一直在 Metaspace 里堆着。反复部署几次,元空间就满了。

另一个主因是上限设太小。默认不设上限才是 Java 8 的设计意图,手动把 MaxMetaspaceSize 压得太低,一个中型应用的正常类量都装不下。

MetaspaceSize 这个参数容易误解,它不是「初始大小」,而是触发一次 full GC 尝试卸载类的阈值。到不了这个值,JVM 不会主动去回收类元数据。

The Fix(解决方案)

报错先别急着找 bug,确认是不是上限卡死了:

java -XX:MaxMetaspaceSize=512m -jar myapp.jar

或者干脆去掉 -XX:MaxMetaspaceSize,交给 JVM 自动扩展。Atlassian 的官方排查文档给的就是这个建议:让 Java 8 按需自动增加 Metaspace。

上限放开了还涨,那就是有 ClassLoader 一直没被回收。看 Metaspace 用量用 jstat:

jstat -gcmetacapacity <pid>

关注 MC(当前元数据容量)和 MU(已用)是不是一路上涨不回落。再用 jcmd <pid> GC.class_stats 看每个类加载器名下占了多少类、多少字节。定位到泄漏的 loader 后,切断引用链:清空静态字段旧引用、停掉没退出的线程、别让框架缓存一直抓着旧 ClassLoader。

动态生成类要收敛。CGLIB、ASM 默认会缓存生成的类,别为了省事关掉缓存。enhancer.setUseCache(true) 能让同类名代理复用,不重复塞元数据。反射和动态代理也一样,能复用就复用。

作为缓解,把 MetaspaceSize 调低能让触发类卸载的 GC 来得更早:

java -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=512m -jar myapp.jar

这只是缓解,不治本。真泄漏还得回源头掐断引用链。

References(参考来源)

最后更新于 2026-09-09

这篇帮到你了吗?

刚解决了一个棘手的报错?花两分钟记录下来,帮助下一个遇到同样问题的开发者。

贡献一条解法