bigywater.tech
Java · 298

类卸载与元空间回收

类什么时候能被卸载?哪些常见写法会让整个类加载器(和它所有的类)永远留在内存里?这篇用 WeakReference 判断,逐种制造泄漏,再用 Metaspace 上限把后果跑出来。

8 种
让类加载器无法回收的泄漏方式(都实测存活)
19053
不保留加载器时 20000 个加载器循环中累计卸载的类数
2556
保留加载器时 Metaspace 16 MB 下触发 OutOfMemoryError 的加载器序号

谁拦住了卸载

实测 · 本机执行输出
  [没有任何泄漏]
    场景 clean                                             → 加载器已被回收;已加载 771,累计卸载 1;Class 对象已回收
  [静态字段里留着该类的一个实例]
    场景 keepObject                                        → 加载器仍然存活;已加载 772,累计卸载 0;Class 对象仍在
  [静态字段里留着 Class 对象]
    场景 keepClass                                         → 加载器仍然存活;已加载 772,累计卸载 0;Class 对象仍在
  [静态字段里留着类加载器]
    场景 keepLoader                                        → 加载器仍然存活;已加载 772,累计卸载 0;Class 对象仍在
  [main 线程的 ThreadLocal 里留着实例]
    场景 threadLocal                                       → 加载器仍然存活;已加载 774,累计卸载 0;Class 对象仍在
  [启动了一个线程,上下文类加载器设为它]
    场景 thread                                            → 加载器仍然存活;已加载 774,累计卸载 0;Class 对象仍在
  [注册了一个持有实例的 shutdown hook]
    场景 shutdownHook                                      → 加载器仍然存活;已加载 780,累计卸载 0;Class 对象仍在
  [线程池线程的 ThreadLocal 里留着实例]
    场景 executor                                          → 加载器仍然存活;已加载 819,累计卸载 0;Class 对象仍在
  [把持有实例的 MBean 注册到 MBeanServer]
    场景 mbean                                             → 加载器仍然存活;已加载 1516,累计卸载 0;Class 对象仍在

日志与开关

实测 · 本机执行输出
  默认                     日志里 unloading class 行数 1;示例 unloading class Payload 0x0000007001001000;加载器已被回收;已加载 771,累计卸载 1;Class 对象已回收
  -XX:-ClassUnloading    日志里 unloading class 行数 0;示例 (无);加载器仍然存活;已加载 772,累计卸载 0;Class 对象仍在

各收集器

实测 · 本机执行输出
  JDK 21 UseSerialGC      加载器已被回收;已加载 771,累计卸载 1;Class 对象已回收
  JDK 21 UseParallelGC    加载器已被回收;已加载 771,累计卸载 1;Class 对象已回收
  JDK 21 UseG1GC          加载器已被回收;已加载 771,累计卸载 1;Class 对象已回收
  JDK 21 UseZGC           加载器已被回收;已加载 800,累计卸载 1;Class 对象已回收
  JDK 21 UseShenandoahGC  加载器已被回收;已加载 797,累计卸载 4;Class 对象已回收
  JDK 27 UseSerialGC      加载器已被回收;已加载 807,累计卸载 1;Class 对象已回收
  JDK 27 UseParallelGC    加载器已被回收;已加载 807,累计卸载 1;Class 对象已回收
  JDK 27 UseG1GC          加载器已被回收;已加载 807,累计卸载 1;Class 对象已回收
  JDK 27 UseZGC           加载器已被回收;已加载 807,累计卸载 1;Class 对象已回收
  JDK 27 UseShenandoahGC  加载器已被回收;已加载 807,累计卸载 1;Class 对象已回收

Metaspace

实测 · 本机执行输出
  不保留加载器(可卸载):
    第 0 个加载器:Metaspace 已用 0 MB,累计卸载类 0
    第 5000 个加载器:Metaspace 已用 3 MB,累计卸载类 3309
    第 10000 个加载器:Metaspace 已用 3 MB,累计卸载类 8630
    第 15000 个加载器:Metaspace 已用 2 MB,累计卸载类 13953
    全部 20000 个完成;Metaspace 已用 2 MB,累计卸载类 19053
  保留所有加载器:
    第 0 个加载器:Metaspace 已用 0 MB,累计卸载类 0
    在第 2556 个加载器时失败:java.lang.OutOfMemoryError: Metaspace
  不保留加载器,但 -XX:-ClassUnloading:
    第 0 个加载器:Metaspace 已用 0 MB,累计卸载类 0
    在第 2550 个加载器时失败:java.lang.OutOfMemoryError: Metaspace

分步演示

类卸载
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK(Temurin 17.0.20、Homebrew 21.0.12 与 27,每篇注明实际用的版本);用 javac 编译、java 直接运行,需要第三方库时用 Maven 解析依赖;计时用 System.nanoTime,数字每次运行会有小幅抖动。JDK 21.0.12 与 27;每个场景新起 JVM,强引用在单独的方法里创建以避免解释器栈槽残留
    • 没有验证:真实 Web 容器热部署的泄漏(JDBC Driver、日志框架等)、G1 并发周期里(而不是 System.gc())的卸载时机、-XX:MetaspaceSize 触发 GC 的阈值行为、Class Data Sharing 对卸载的影响、JIT 编译代码(nmethod)对加载器的引用
    京ICP备2026067502号-1京公网安备11010502063487号