Skip to content

方法区垃圾回收

方法区也会 GC

很多人以为 GC 只回收堆,实际上方法区(元空间)也需要垃圾回收。只是方法区的 GC 频率比堆低得多,回收效率也不如堆明显。

方法区的 GC 主要关注两部分:

  1. 常量池的垃圾回收:废弃的常量
  2. 类的卸载:不再使用的类

常量池的垃圾回收

运行时常量池中,如果一个字符串常量不再被任何地方引用,GC 时可以被回收。

java
public class ConstantGC {
    public static void main(String[] args) {
        // 生成大量字符串
        for (int i = 0; i < 100000; i++) {
            String s = ("key_" + i).intern();
            // 如果这些字符串之后没有被引用
            // GC 时可以被回收
        }
        // 此时,之前 intern 的字符串如果没被持有引用
        // 可以在常量池 GC 时被回收
    }
}

类的卸载:方法区 GC 的核心

类的生命周期

一个类什么时候会被卸载(从方法区移除)?

当满足以下所有条件时,类可以被卸载:

  1. 该类的所有实例已被回收:堆中没有任何该类的对象
  2. 加载该类的 ClassLoader 已被回收:自定义类加载器没有被引用
  3. 该类的 Class 对象没有被引用:没有代码持有该类的 Class 对象
java
public class ClassUnloading {
    public static void main(String[] args) throws Exception {
        // 自定义类加载器
        ClassLoader loader = new MyClassLoader();

        // 加载类
        Class&lt;?&gt; clazz = loader.loadClass("com.example.TestClass");

        // 使用类
        Object obj = clazz.newInstance();

        // 现在,如果:
        // 1. obj 被 GC 回收(没有强引用)
        // 2. loader 不再被引用
        // 3. clazz 不再被引用
        // 那么 TestClass 可能被卸载
        obj = null;
        loader = null;
        clazz = null;

        // Full GC 后,方法区可能释放 TestClass 占用的空间
        System.gc();
    }
}

引导类加载器加载的类不会卸载

由 Bootstrap ClassLoader 加载的核心类(java.lang.String 等),永远不会卸载。这是合理的——如果 String 被卸载了,整个 Java 世界就崩溃了。

应用类加载器加载的类可能卸载

由 Application ClassLoader(系统类加载器)加载的类,理论上可以被卸载,但实际卸载率很低

方法区 GC 的触发条件

java
public class MetaspaceGC {
    public static void main(String[] args) throws Exception {
        // 方法区 GC 通常在 Full GC 时一起触发

        // 手动触发 Full GC
        System.gc();  // 建议触发,不保证

        // 在某些场景下,方法区 GC 会自动发生:
        // 1. 元空间使用达到阈值
        // 2. 显式的 System.gc()
        // 3. Full GC 时
    }
}

元空间 GC 的特点

元空间使用达到 MetaspaceSize

当元空间使用量达到 MetaspaceSize 时,会触发一次 Metaspace GC——这次 GC 会尝试回收不再使用的类信息。

bash
# JDK 8 中,MetaspaceSize 默认约 21MB
# 达到阈值时,会触发 Metaspace GC
# 如果 GC 后仍然没有足够空间,会触发类加载 OOM

元空间 GC vs 堆 GC

维度堆 GC元空间 GC
触发频率高(MinorGC 每分钟多次)低(通常只在 FullGC 时)
回收效果明显(通常能回收大量对象)不明显(类卸载率低)
停顿影响主要停顿来源通常附属于 FullGC
OOM 风险堆满元空间满

类卸载的验证

java
public class VerifyUnloading {
    public static void main(String[] args) throws Exception {
        // 启用类卸载日志
        // java -XX:+TraceClassUnloading
    }
}
bash
# 开启类卸载日志
java -XX:+TraceClassUnloading \
     -XX:+TraceClassLoading \
     MyApp
# 输出:
# [ClassUnloading] Class 'com.example.Test' has been unloaded
# [ClassLoading] Class 'com.example.Test' has been loaded

常见误区

误区一:类加载了就不能卸载

实际上,自定义类加载器加载的类在满足条件时可以卸载。只是因为类加载器的引用通常被长期持有,卸载条件很难满足。

误区二:元空间不需要 GC

元空间 GC 确实不是主要矛盾,但如果元空间设置过小(比如 -XX:MaxMetaspaceSize=64m),元空间 GC 可能会频繁触发,影响性能。

误区三:Spring Boot 应用不需要担心元空间

Spring Boot 应用大量使用反射、动态代理、CGLib,会生成大量代理类。如果使用 Tomcat 等传统容器,每个 Webapp 的类加载器在应用重载时会持有旧类引用,可能导致元空间压力。

实战建议

场景建议
大型 Spring Boot 应用-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m
使用 OSGi/热部署增大元空间,启用类卸载日志
减少类卸载问题避免频繁创建/销毁自定义类加载器
诊断类加载问题启用 -XX:+TraceClassUnloading

本节小结

方法区垃圾回收的核心要点:

维度说明
常量回收不再被引用的字符串常量可被回收
类卸载条件所有实例已回收 + ClassLoader 已回收 + Class 对象无引用
永不卸载Bootstrap 加载的核心类(String、Object 等)
触发时机元空间达到阈值、Full GC 时
回收效果实际卸载率低,不是主要 GC 目标

方法区 GC 不是性能优化的重点,但如果元空间设置不当,仍然可能引发 OOM。

到这里,方法区(元空间)的内容全部完成。接下来进入 直接内存 部分——直接内存(使用/测试/OOM)

基于 VitePress 构建