方法区垃圾回收
方法区也会 GC
很多人以为 GC 只回收堆,实际上方法区(元空间)也需要垃圾回收。只是方法区的 GC 频率比堆低得多,回收效率也不如堆明显。
方法区的 GC 主要关注两部分:
- 常量池的垃圾回收:废弃的常量
- 类的卸载:不再使用的类
常量池的垃圾回收
运行时常量池中,如果一个字符串常量不再被任何地方引用,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 时被回收
}
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
类的卸载:方法区 GC 的核心
类的生命周期
一个类什么时候会被卸载(从方法区移除)?
当满足以下所有条件时,类可以被卸载:
- 该类的所有实例已被回收:堆中没有任何该类的对象
- 加载该类的 ClassLoader 已被回收:自定义类加载器没有被引用
- 该类的 Class 对象没有被引用:没有代码持有该类的
Class对象
java
public class ClassUnloading {
public static void main(String[] args) throws Exception {
// 自定义类加载器
ClassLoader loader = new MyClassLoader();
// 加载类
Class<?> 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();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
引导类加载器加载的类不会卸载
由 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 时
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
元空间 GC 的特点
元空间使用达到 MetaspaceSize
当元空间使用量达到 MetaspaceSize 时,会触发一次 Metaspace GC——这次 GC 会尝试回收不再使用的类信息。
bash
# JDK 8 中,MetaspaceSize 默认约 21MB
# 达到阈值时,会触发 Metaspace GC
# 如果 GC 后仍然没有足够空间,会触发类加载 OOM1
2
3
2
3
元空间 GC vs 堆 GC
| 维度 | 堆 GC | 元空间 GC |
|---|---|---|
| 触发频率 | 高(MinorGC 每分钟多次) | 低(通常只在 FullGC 时) |
| 回收效果 | 明显(通常能回收大量对象) | 不明显(类卸载率低) |
| 停顿影响 | 主要停顿来源 | 通常附属于 FullGC |
| OOM 风险 | 堆满 | 元空间满 |
类卸载的验证
java
public class VerifyUnloading {
public static void main(String[] args) throws Exception {
// 启用类卸载日志
// java -XX:+TraceClassUnloading
}
}1
2
3
4
5
6
2
3
4
5
6
bash
# 开启类卸载日志
java -XX:+TraceClassUnloading \
-XX:+TraceClassLoading \
MyApp
# 输出:
# [ClassUnloading] Class 'com.example.Test' has been unloaded
# [ClassLoading] Class 'com.example.Test' has been loaded1
2
3
4
5
6
7
2
3
4
5
6
7
常见误区
误区一:类加载了就不能卸载
实际上,自定义类加载器加载的类在满足条件时可以卸载。只是因为类加载器的引用通常被长期持有,卸载条件很难满足。
误区二:元空间不需要 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)。
