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
这只是缓解,不治本。真泄漏还得回源头掐断引用链。
