JVM类卸载是被动严苛的回收动作,需同时满足三个条件且恰逢Full GC:所有该类实例(含隐式引用)被彻底回收、加载它的ClassLoader被GC回收、对应Class对象无任何强引用;仅自定义ClassLoader加载的类可能卸载,系统类加载器永不回收。

JVM 类卸载不是主动行为,而是一次被动、严苛的回收动作,必须等三个条件全部满足,且恰逢 Full GC 时才会发生。它不响应内存压力本身,也不在 Minor GC 或常规运行中触发。
所有该类的实例对象必须被彻底回收
不只是 new 出来的对象要消失,还包括:内部类隐式持有的外部类引用、静态集合里缓存的对象、线程局部变量(ThreadLocal)中残留的实例、异步任务未完成导致的线程池隐式持有等。哪怕一个 static List<MyService> cache 还存着某个服务实例,整个类就仍被视为“存活”。
- 常见疏漏:缓存未 clear()、监听器注册后没反注册、shutdown() 方法未调用
- 特别注意:子类实例也会阻止父类卸载;finalizer 或 Cleaner 关联的对象可能延迟回收
加载它的 ClassLoader 实例必须被 GC 回收
ClassLoader 是类元数据的归属主体,Metaspace 按加载器隔离管理。系统类加载器(Bootstrap、Platform、AppClassLoader)生命周期与 JVM 绑定,永不回收;真正可卸载的,只来自自定义加载器(如 WebAppClassLoader、URLClassLoader、OSGi BundleClassLoader)。
- 只要它被静态字段、线程上下文类加载器(Thread.currentThread().getContextClassLoader())、Spring 上下文或日志框架间接持有,就无法回收
- Web 应用重启时,若线程池未关闭、ServletContext 未清理、Filter/Listener 未注销,ClassLoader 就会泄漏
该类对应的 Class 对象不能有任何强引用
这是最隐蔽的泄漏点。Class 对象常被无意识缓存,哪怕实例和加载器都已释放,只要 Class 还被拿着,卸载就失败。
- 典型场景:static Map<String, Class> 反射缓存、ThreadLocal<Class> 使用后未 remove()
- 反射操作如 Method.setAccessible(true) 后未恢复,或 JNI 调用 NewGlobalRef 后遗漏 DeleteGlobalRef
- Spring 的类型注册表、Hibernate 的元模型缓存、某些 AOP 代理生成逻辑也会长期持有 Class 引用
仅在 Full GC(或 G1/CMS 并发标记完成阶段)中触发
类卸载逻辑不参与任何年轻代回收,只嵌入在 Full GC 流程中。Metaspace 的真正释放始于此时,但释放后的内存不一定立刻还给操作系统。
- G1 中自动集成,无需额外参数;CMS 需启用 -XX:+CMSClassUnloadingEnabled
- 可通过 -XX:+TraceClassUnloading 查看卸载日志,确认是否成功
- Metaspace 空闲块优先复用,仅当空闲率超 -XX:MaxMetaspaceFreeRatio(默认 70%)且有连续大块时,才尝试返还 OS

















