# 50. 什么是类卸载?为什么JDK 8的Metaspace可能导致OOM?

# 标准答案

类卸载(Class Unloading)是 JVM 通过垃圾回收(GC)移除 无引用的类及其元数据,以释放 元空间(Metaspace)永久代(PermGen) 的内存。类卸载通常发生在 Full GC 过程中,前提是对应的 ClassLoader 及其加载的所有类实例 都不可达。

在 JDK 8 之前,类元数据存储在 永久代(PermGen),但其固定大小易引发 OOM。JDK 8 移除了永久代,引入 Metaspace,将类元数据存储在 本地内存(Native Memory),避免 JVM 内部固定大小的限制。然而,Metaspace 并不等于无限大,若未合理配置,仍可能因 类加载过多(如动态代理、热加载)导致 OOM

# 答案解析

# 1. 什么是类卸载?

类卸载指 JVM 通过 GC 回收无用的类元数据,释放 Metaspace 内存。类卸载的前提是:

  1. ClassLoader 不再被引用**(通常是自定义 ClassLoader)。
  2. ClassLoader 加载的所有类实例都被回收**(即 Heap 中没有存活对象)。
  3. 类对象本身无存活引用**(如 ThreadLocal 没有持有 Class)。

类卸载通常由 Full GC 触发,GC 日志中 Class unloading 表示发生了类卸载。

[Full GC (Metadata GC Threshold)  512M->256M, 0.5 secs]
Unloading class com.example.MyClass
1
2

# 2. JDK 8 及以后,为什么 Metaspace 可能导致 OOM?

JDK 8 移除了 PermGen,引入 Metaspace,将类元数据存放在 本地内存(Native Memory),而不是 JVM 堆(Heap)。但 Metaspace 不等于无限大,仍可能导致 OOM,主要原因包括:

  1. 大量动态类加载

    • Spring、Hibernate 动态代理(CGLIB、JDK Proxy)生成大量代理类,导致 Metaspace 持续增长。
    • 反复 加载/卸载 ClassLoader,如 Tomcat 频繁热部署,导致元数据无法及时回收。
  2. Metaspace 默认不受 -Xmx 限制

    • PermGen-XX:MaxPermSize 限制,但 Metaspace 默认只受物理内存限制,如果不设置 MaxMetaspaceSize,可能无限增长导致 OOM。
  3. 类卸载滞后

    • Full GC 触发类卸载,而 JVM 可能避免频繁 Full GC,导致 Metaspace 积累大量废弃类,最终 OOM。

# 3. 代码示例

以下代码演示 动态生成大量类,导致 Metaspace OOM

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;

public class MetaspaceOOM {
    public static void main(String[] args) {
        while (true) {
            Enhancer enhancer = new Enhancer();
            enhancer.setSuperclass(OOMObject.class);
            enhancer.setUseCache(false);
            enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) -> proxy.invokeSuper(obj, args1));
            enhancer.create();  // 创建动态代理类,持续占用 Metaspace
        }
    }

    static class OOMObject {}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

运行参数:

-XX:MaxMetaspaceSize=10M  # 限制 Metaspace 仅 10MB
1

错误日志:

java.lang.OutOfMemoryError: Metaspace
1

# 4. 如何优化 Metaspace,避免 OOM?

  1. 合理设置 MaxMetaspaceSize

    • -XX:MaxMetaspaceSize=256M 限制 Metaspace 避免无限增长。
    • -XX:MetaspaceSize=128M 设置初始 Metaspace,防止频繁 GC。
  2. 避免类加载泄漏

    • 检查 反复创建 ClassLoader 却不卸载 的情况,如 Tomcat 热部署
    • 避免 ThreadLocal 持有 Class,阻止类卸载。
  3. 使用 jmapjstat 排查 Metaspace

    • jmap -clstats <pid> 查看类加载信息。
    • jstat -gcmetacapacity <pid> 监控 Metaspace 增长情况。
  4. 手动触发 Full GC

    • System.gc() 不保证触发 类卸载,JVM 可能忽略调用。
    • 使用 jcmd <pid> GC.run 强制 Full GC,尝试卸载类。

# 深入追问

  1. 为什么 Metaspace OOM 可能导致整个 JVM 崩溃?
  2. Spring AOP 生成代理类时,如何避免 Metaspace OOM?
  3. JDK 11 引入的 Class-Data Sharing(CDS)如何优化 Metaspace?

# 相关面试题

  • 为什么 JDK 8 移除了永久代(PermGen)?
  • JVM 何时会触发类卸载?如何查看类卸载日志?
  • 如何优化 Tomcat 的热部署,避免 Metaspace OOM?