适合人群: Java工程师、运维工程师
难度等级: ⭐⭐⭐⭐ (中高级)
阅读时间: 12分钟
📖 引言:一个神秘的OOM
错误信息:
java.lang.OutOfMemoryError: Metaspace
什么?堆内存明明还有很多,为什么OOM?🤔
🏗️ 第一章:方法区 vs 元空间
JDK 7及之前:永久代 (PermGen)
堆内存的一部分
存储:类信息、常量、静态变量
参数:
-XX:PermSize=128m
-XX:MaxPermSize=256m
问题:
❌ 大小固定,容易溢出
❌ GC效率低
JDK 8+:元空间 (Metaspace)
使用本地内存(Native Memory)
存储:类元数据
参数:
-XX:MetaspaceSize=128m # 初始大小
-XX:MaxMetaspaceSize=512m # 最大大小(默认无限)
优点:
✅ 使用本地内存,不占堆
✅ 默认无限大(受限于物理内存)
✅ GC效率更高
🔥 第二章:元空间溢出的5大原因
原因1:大量使用CGLib动态代理 🎭
// 每次都生成新类!
for (int i = 0; i < 100000; i++) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MyClass.class);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object obj, Method method,
Object[] args, MethodProxy proxy) {
return proxy.invokeSuper(obj, args);
}
});
MyClass proxy = (MyClass) enhancer.create(); // 💥 生成新类
}
// 10万个类 → 元空间爆满!
解决方案:
// ✅ 复用代理类
Map<Class<?>, Object> proxyCache = new ConcurrentHashMap<>();
Object proxy = proxyCache.computeIfAbsent(MyClass.class, k -> {
// 只创建一次
return createProxy(k);
});
原因2:Groovy/JSP动态脚本 📜
// Groovy动态编译
for (int i = 0; i < 10000; i++) {
GroovyShell shell = new GroovyShell();
shell.evaluate("println 'Hello " + i + "'"); // 💥 每次生成新类
}
// JSP也一样:每个JSP文件编译成一个类
解决方案:
// ✅ 复用GroovyShell
GroovyShell shell = new GroovyShell();
Script script = shell.parse("println message");
script.setProperty("message", "Hello");
script.run();
原因3:大量使用反射 🔍
// 反射生成的类也占用元空间
for (int i = 0; i < 100000; i++) {
Class<?> clazz = Class.forName("com.example.MyClass" + i);
}
原因4:过多的第三方JAR包 📚
一个大型项目:
- 500+ JAR包
- 10000+ 类
- 元空间占用:200MB+
如果MaxMetaspaceSize设置太小 → OOM
查看类加载情况:
jcmd <pid> GC.class_stats | grep -v java | head -20
# 输出:
# 类数量:15234
# 元空间使用:256MB
原因5:类加载器泄漏 💧
// Web应用重新部署时
// 旧的WebAppClassLoader没有被回收
// 导致旧类无法卸载
原因:
- 静态变量持有类引用
- ThreadLocal没清理
- 定时任务没停止
解决方案:
// ✅ 应用卸载时清理
@Override
public void contextDestroyed(ServletContextEvent sce) {
// 停止定时任务
scheduler.shutdown();
// 清理ThreadLocal
ThreadLocalCleaner.clean();
// 清理静态变量
MyClass.staticCache = null;
}
🛠️ 第三章:排查和解决
排查步骤 🔍
# 1. 查看元空间使用情况
jstat -gc <pid>
# 关注 MC(元空间容量)和 MU(元空间使用)
# 2. Dump元空间
jcmd <pid> GC.class_histogram
# 3. 查看类加载器
jmap -clstats <pid>
# 4. 生成Heap Dump分析
jmap -dump:live,format=b,file=heap.hprof <pid>
# 用MAT查看类加载器和类数量
参数调优 ⚙️
# 合理设置元空间大小
-XX:MetaspaceSize=256m # 初始大小(触发GC的阈值)
-XX:MaxMetaspaceSize=512m # 最大大小
# 监控元空间GC
-XX:+TraceClassLoading # 跟踪类加载
-XX:+TraceClassUnloading # 跟踪类卸载
# 不要设置太小!
# 经验值:
# - 小型应用:256MB
# - 中型应用:512MB
# - 大型应用:1GB+
💡 总结
元空间溢出的常见原因:
1. CGLib动态代理(没复用)
2. Groovy/JSP动态脚本
3. 反射大量加载类
4. JAR包太多
5. 类加载器泄漏
解决方案:
✅ 复用代理类
✅ 合理设置元空间大小
✅ 避免类加载器泄漏
✅ 监控类数量
记住:元空间不是无限的!要监控!
下一篇: 大对象直接进入老年代的设计