Java类卸载必须同时满足三个条件:所有实例被回收、ClassLoader被回收、Class对象无任何强引用;仅自定义类加载器加载的类才可能卸载,且仅在Full GC或G1/CMS并发标记完成阶段触发。

Java 类卸载不是 GC 主动发起的操作,而是一套被动、严苛、以类加载器为单位的回收机制。它不会在 Minor GC 中发生,也不响应内存压力自动触发,只有当三个条件同时满足,并恰逢特定 GC 阶段时,才可能批量卸载。
类卸载必须同时满足的三个硬性条件
缺一不可,任一不成立,该类及其元数据就永远留在 Metaspace 中:
-
该类所有 Java 堆实例已被垃圾回收:包括直接 new 的对象、子类实例、内部类隐式持有的外部类引用、静态集合缓存(如
static List<MyService> cache)、ThreadLocal 中残留的对象等。哪怕一个强引用还存在,整个类就被视为“存活”。 -
加载它的 ClassLoader 实例已被 GC 回收:ClassLoader 是类元数据的“主人”,Metaspace 按加载器隔离存储。系统类加载器(Bootstrap/Platform/AppClassLoader)生命周期与 JVM 绑定,基本不可回收;真正可卸载的,只来自自定义加载器(如 WebAppClassLoader、URLClassLoader、OSGi BundleClassLoader)。但只要它被静态字段、线程上下文类加载器(
Thread.currentThread().getContextClassLoader())、Spring 上下文或日志框架间接持有,就无法回收。 -
该类对应的
java.lang.Class对象无任何强引用:这是最隐蔽的泄漏点。常见场景包括:static Map<String, Class>类型的反射缓存未清理、ThreadLocal<Class>使用后未调用remove()、JNI 全局引用未释放、Spring 类型注册表或 CGLIB 代理生成时残留的Method.getDeclaringClass()引用等。
GC 中实际触发类卸载的时机
即使三条件全部满足,卸载也不会立即执行,必须依赖特定 GC 事件:
- 多数情况下需等待一次 Full GC(如 Parallel GC、CMS 或 G1 的 Full GC 阶段);
- G1 收集器 在并发标记周期(Concurrent Marking Cycle)结束时检查并卸载符合条件的类;
- 当设置
-XX:MaxMetaspaceSize且 Metaspace 接近上限时,JVM 会主动触发 GC 尝试类卸载,但是否成功仍取决于上述三条件; -
System.gc()仅是建议,现代 JVM 通常忽略,不能保证触发卸载。
哪些场景真正能促成类卸载
这些场景天然有利于三条件同时成立:
立即学习“Java免费学习笔记(深入)”;
- Web 容器热部署/应用卸载:如 Tomcat 停止 WebApp,其 WebAppClassLoader 失去所有引用,业务类实例销毁,Class 对象无外部强引用;
- 动态代理类生命周期结束:Spring AOP 或 CGLIB 生成的代理类,若其依赖的自定义类加载器被回收,且代理对象全部销毁、Class 引用清除,元数据可被卸载;
- OSGi 或模块化框架卸载模块:每个模块独占类加载器,模块卸载即类加载器失效,连带其加载的全部类元数据被批量清理。
验证类是否真正卸载的方法
不能只看 Metaspace 内存下降,那可能是加载失败回滚或其他原因:
- 启用
-XX:+TraceClassUnloading,观察 GC 日志中是否出现类似[Unloading class com.example.PluginService]的输出; - 配合
jcmd <pid> VM.native_memory summary scale=MB对比前后 “class” 区域变化; - 用
jmap -histo:live确认目标类实例数归零; - JFR(Java Flight Recorder)录制并筛选
ClassUnloading事件,是最可靠的运行时证据。


















