java类卸载必须同时满足三个条件:所有实例被回收、classloader被回收、class对象无任何引用;仅自定义类加载器加载的类才可能卸载,且仅在full gc或g1/cms并发标记完成阶段触发。

Java 程序运行中,类卸载不是常规操作,而是一套被动、严苛、以类加载器为单位的回收机制。它不会因内存紧张自动发生,也不响应日常 Minor GC,只有在特定条件下,且恰逢 Full GC(或 G1/CMS 的并发标记完成阶段)时,才可能批量触发。
必须同时满足三个硬性条件,缺一不可:
该类的所有 Java 堆实例已被垃圾回收
包括直接 new 出的对象、子类实例、内部类隐式持有的外部类引用、静态集合缓存(如static List<myservice> cache</myservice>)、线程局部变量(ThreadLocal<myservice></myservice>)中的残留对象等。哪怕一个对象还被强引用着,整个类就视为“存活”。-
加载它的 ClassLoader 实例已被 GC 回收
ClassLoader 是类元数据的“主人”,Metaspace 按加载器隔离存储。系统类加载器(Bootstrap、Platform、AppClassLoader)生命周期与 JVM 绑定,基本不可回收;真正可卸载的,只来自自定义加载器(如WebAppClassLoader、URLClassLoader、OSGi BundleClassLoader)。但只要它被以下任一路径间接持有,就无法回收:- 静态字段(如
private static ClassLoader HOLDER) - 线程上下文类加载器未重置(
Thread.currentThread().setContextClassLoader(null)缺失) - Spring 上下文、Servlet 容器、Logback 等框架未释放对其的引用
- 静态字段(如
-
该类对应的
java.lang.Class对象无任何强引用
这是最隐蔽的泄漏点。即使实例清空、加载器置 null,只要 Class 对象还在被拿着,卸载就失败。常见场景包括:-
static Map<string class>></string>类型的反射缓存未清理 -
ThreadLocal<class>></class>使用后未调用remove() - 反射调用
Method.setAccessible(true)后未恢复或未清理句柄 - JNI 层调用
NewGlobalRef(env, clazz)后遗漏DeleteGlobalRef
-
触发时机仅限于 Full GC 阶段,且依赖 JVM 参数支持:
- 类卸载逻辑只在 Full GC(或 G1/CMS 并发标记完成)中执行,不响应 Metaspace 使用率上涨
- 默认开启
-XX:+ClassUnloading(HotSpot),但仅对满足上述条件的类生效 - 可通过
-XX:+TraceClassUnloading开启日志,观察[Unloading class xxx]输出来确认是否真实卸载 -
-Xnoclassgc会彻底禁用类卸载(慎用)
真正能触发类卸载的典型场景有限:
- Web 容器重启应用(如 Tomcat 停用旧
WebAppClassLoader) - OSGi 模块热部署/卸载
- 插件化系统中手动销毁
URLClassLoader并确保所有引用已清除 - Spring Boot DevTools 热加载时新旧 ClassLoader 切换成功
不复杂但容易忽略
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











