java中类卸载需同时满足三个条件:所有实例被gc回收、classloader实例被回收、class对象无任何引用;自定义类加载器不回收则类无法卸载,因其是元数据的“主人”,metaspace按classloader隔离存储,jvm仅在其不可达时扫描卸载。

Java中类不能随便卸载——必须同时满足三个硬性条件,缺一不可:该类所有实例已被GC回收;加载它的ClassLoader实例本身已被回收;这个类对应的Class对象没被任何地方引用(包括静态字段、缓存、ThreadLocal、反射句柄等)。
为什么自定义类加载器不回收,类就永远卸不掉?
类加载器不是“工具”,而是元数据的“主人”。Metaspace里每个类的元数据都按ClassLoader隔离存放,JVM只在确认该加载器彻底不可达时,才扫描它名下的所有类是否可卸载。一旦你的CustomClassLoader被某个静态变量、线程上下文或第三方框架(比如Spring的ContextClassLoader)悄悄持有,它和它加载的所有类就全卡在Metaspace里了。
- 常见错误现象:
OutOfMemoryError: Metaspace反复出现,但堆内存充足;用jstat -gc <pid></pid>看到MU(Metaspace used)持续上涨,MC(Metaspace capacity)也不断扩容 - 实操建议:在类加载器使用完毕后,显式置空所有强引用,尤其检查
Thread.currentThread().setContextClassLoader(null)是否被调用;避免在静态Map<string class></string>里缓存动态加载的类 - 验证方式:加上
-XX:+TraceClassUnloading启动,观察日志里是否出现类似[Unloading class com.example.PluginService]——没这条,基本说明加载器还活着
Class对象被谁悄悄引用了?最隐蔽的三类泄漏源
即使你把类实例全清空、加载器也设为null,只要Class对象本身还被持有着,卸载就失败。这不是逻辑错误,是JVM的强制安全策略。
-
static字段缓存:private static final Map<string class> CACHE = new HashMap()</string>—— 这是最常见的“无意识引用” -
ThreadLocal<class></class>未清理:尤其在Web容器或线程池场景下,一个请求线程用完后没调用threadLocal.remove(),就可能让整个类加载器链无法释放 - JNI或反射残留:比如通过
Method.setAccessible(true)访问过私有方法,或本地代码调用了NewGlobalRef(env, clazz)但忘了DeleteGlobalRef
Full GC才是Metaspace回收的“开关”,但别迷信System.gc()
类卸载只发生在Full GC期间,且仅当启用了对应GC策略的类卸载支持。盲目调用System.gc()几乎无效,它只是启发式提示,JVM完全可以忽略。
立即学习“Java免费学习笔记(深入)”;
- 不同GC器参数差异很大:
– CMS需配-XX:+CMSClassUnloadingEnabled;
– G1在JDK 8u40+后支持-XX:+G1UseConcMarkSweepGC(注意不是默认开启);
– ZGC/Shenandoah则默认支持并发类卸载,无需额外开关 - 性能影响:启用类卸载会增加Full GC的扫描开销,尤其在大量动态类场景下,可能延长停顿时间;但不开,Metaspace迟早OOM
- 实操建议:生产环境优先用G1 +
-XX:+UseG1GC -XX:+G1UseConcMarkSweepGC -XX:+TraceClassUnloading组合,并配合-Xlog:gc*,gc+metaspace=trace持续观测
真正难的不是写对卸载条件,而是追踪“谁还在引用那个Class”——它可能藏在一行不起眼的static final里,也可能在第三方库的ThreadLocal中沉睡。调试时别只盯着自己的代码,先用jcmd <pid> VM.native_memory summary</pid>和jmap -clstats <pid></pid>看类加载器存活数,比猜快得多。










