Java类卸载需同时满足:所有实例被回收、ClassLoader实例被GC、Class对象无强引用,且仅在Full GC时触发;元空间释放始于此时,依赖特定JVM参数启用。

Java 中类卸载不是“用完即走”的常规操作,而是一套由 GC 可达性驱动的被动机制。它不发生在日常 Minor GC 中,也不响应内存使用率上涨本身;只有当三个条件全部满足、且恰逢 Full GC 时,JVM 才会批量卸载某个类加载器名下的所有类——元空间(Metaspace)的真正释放,就始于这一刻。
所有实例对象必须被彻底回收
不只是 new 出来的对象要消失,还包括:子类实例、内部类隐式持有的外部类引用、静态集合中缓存的对象、线程局部变量里残留的实例等。哪怕一个 static List<MyService> cache 还持有某个服务对象,该类的所有实例就仍被视为“存活”,卸载直接终止。
常见疏漏点:
- 未调用清理方法(如 cache.clear() 或 shutdown())
- 监听器、回调注册后未反注册,导致对象长期驻留
- 异步任务提交后未等待完成,对象被线程池隐式引用
加载它的 ClassLoader 实例必须被 GC 回收
ClassLoader 是类元数据的“主人”,Metaspace 按加载器隔离存储。系统类加载器(如 AppClassLoader、PlatformClassLoader)生命周期与 JVM 一致,永不回收;真正可卸载的,只来自自定义加载器——比如 WebAppClassLoader、URLClassLoader、OSGi BundleClassLoader。
但只要它被以下任一路径间接持有着,就无法回收:
- 静态字段(如 private static ClassLoader HOLDER)
- 线程上下文类加载器未重置(Thread.currentThread().setContextClassLoader(null) 缺失)
- Spring 上下文、Servlet 容器、第三方框架(如 Logback、Metrics)未释放对其的引用
Class 对象本身不能有任何强引用
即使实例清空、加载器置 null,只要 java.lang.Class 对象还被拿着,卸载就失败。这是最隐蔽的泄漏源,因为 Class 对象常被无意识缓存:
- static Map<String, Class> 类型的反射缓存
- ThreadLocal<Class> 使用后未调用 remove()
- 反射调用 Method.setAccessible(true) 后未恢复或未清理句柄
- JNI 层调用 NewGlobalRef(env, clazz) 后遗漏 DeleteGlobalRef
Full GC 是唯一触发时机,且需显式启用支持
类卸载逻辑只在 Full GC(或 G1/CMS 的并发标记完成阶段)中执行。JVM 不会因 Metaspace 使用量高就自动启动卸载——它只响应 GC 周期,并依赖配置开关:
- JDK 8:需添加 -XX:+CMSClassUnloadingEnabled(配合 CMS)
- JDK 9+:启用 G1 时需配 -XX:+G1UseConcMarkSweepGC 或确保 G1 并发类卸载开启
- 统一验证手段:-XX:+TraceClassUnloading,日志中出现 Unloading class xxx 才算真实发生

















