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

Java 中类卸载不是主动行为,而是一套被动、严苛的回收机制。它不会因内存紧张自动发生,也不响应常规 Minor GC,只有在特定条件全部满足且恰逢 Full GC(或 G1/CMS 并发标记完成阶段)时,才可能批量触发。
类卸载必须同时满足的三个硬性条件
缺一不可,任一不成立,该类就“卡”在 Metaspace 里无法释放:
-
该类所有 Java 堆实例已被垃圾回收:包括直接 new 的对象、子类实例、内部类隐式持有的外部类引用、静态集合缓存(如
static List<MyService> cache)、ThreadLocal<MyService>中残留对象等。哪怕一个强引用还存在,整个类就被视为“存活”。 -
加载它的 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()、Spring 等框架将 Class 注册进类型注册表、JNI 全局引用未配对释放(NewGlobalRef/DeleteGlobalRef)等。
类卸载的实际触发时机
即使三条件全部满足,卸载也不会立即执行,而是依赖 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失去所有引用,所加载的 Controller、Service 等实例已销毁,Class 对象也未被框架长期缓存; - OSGi 或模块化框架卸载模块:每个模块使用独立类加载器,模块卸载即类加载器失效,连带其加载的全部类元数据被批量清理;
-
动态插件系统:手动使用
URLClassLoader加载插件后,显式置空所有强引用、重置线程上下文类加载器为null、清空反射缓存,并确保无 JNI 全局引用残留。
如何验证类是否真的卸载了
不能只看监控指标下降,必须结合日志和工具交叉确认:
- 启动参数添加
-XX:+TraceClassUnloading,观察 GC 日志中是否出现类似[Unloading class com.example.PluginService 0x00000008002a1000]的输出; - 用
jcmd <pid> VM.native_memory summary scale=MB对比前后 “class” 区域变化; - 配合
jmap -histo:live确认目标类实例数归零; - 注意:
jstat -gc的 MU(Metaspace used)下降 ≠ 类卸载,可能是类加载失败回滚;jconsole中 Metaspace Usage.used 下降也只说明元空间用得少了,不等于类被卸载。


















