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

Java 程序运行中,类卸载不是常规操作,而是一套被动、严苛、以类加载器为单位的回收机制。它不会因内存紧张自动发生,也不响应日常 Minor GC,只有在特定条件下,且恰逢 Full GC(或 G1/CMS 的并发标记完成阶段)时,才可能批量触发。
必须同时满足三个硬性条件,缺一不可:
该类的所有 Java 堆实例已被垃圾回收
包括直接 new 出的对象、子类实例、内部类隐式持有的外部类引用、静态集合缓存(如static List<MyService> cache)、线程局部变量(ThreadLocal<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<?>>类型的反射缓存未清理 -
ThreadLocal<Class<?>>使用后未调用remove() - 反射调用
Method.setAccessible(true)后未恢复或未清理句柄 - JNI 层调用
NewGlobalRef(env, clazz)后遗漏DeleteGlobalRef
-
触发时机仅限于 Full GC 阶段,且依赖 JVM 参数支持:
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
- 类卸载逻辑只在 Full GC(或 G1/CMS 并发标记完成)中执行,不响应 Metaspace 使用率上涨
- 默认开启
-XX:+ClassUnloading(HotSpot),但仅对满足上述条件的类生效 - 可通过
-XX:+TraceClassUnloading开启日志,观察[Unloading class xxx]输出来确认是否真实卸载 -
-Xnoclassgc会彻底禁用类卸载(慎用)
真正能触发类卸载的典型场景有限:
立即学习“Java免费学习笔记(深入)”;
- Web 容器重启应用(如 Tomcat 停用旧
WebAppClassLoader) - OSGi 模块热部署/卸载
- 插件化系统中手动销毁
URLClassLoader并确保所有引用已清除 - Spring Boot DevTools 热加载时新旧 ClassLoader 切换成功
不复杂但容易忽略

















