💥 方法区(元空间)溢出:为什么你的应用突然"爆炸"了?

28 阅读3分钟

适合人群: 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. 类加载器泄漏

解决方案:
✅ 复用代理类
✅ 合理设置元空间大小
✅ 避免类加载器泄漏
✅ 监控类数量

记住:元空间不是无限的!要监控!

下一篇: 大对象直接进入老年代的设计