java类卸载需同时满足三个条件:该类所有实例已被gc回收、其classloader实例已被回收、该类class对象无任何强引用;三者缺一不可,且仅在full gc时被动触发,成功卸载需通过-xx:+traceclassunloading日志确认。

Java 类卸载不是主动触发的操作,而是 JVM 在 Full GC 过程中,基于可达性分析被动判定并清理元信息的一套严格机制。它不依赖“是否还在用”,而取决于三类对象是否全部不可达:类实例、ClassLoader 实例、Class 对象本身。
三个判定条件必须同时满足
JVM 不会单独检查某一项,而是把这三项当作原子前提——任一不成立,对应类的元数据就永久保留在 Metaspace 中:
-
该类所有 Java 堆实例已被 GC 回收:包括显式 new 的对象、数组元素、内部类隐式持有的外部类实例;静态集合(如
static List<myservice></myservice>)里残留一个引用,整个类就无法清空 -
加载它的 ClassLoader 实例已被 GC 回收:ClassLoader 是元数据的“归属者”,Metaspace 按加载器隔离存储;只要它被静态字段、线程上下文(
Thread.currentThread().getContextClassLoader())、Spring 上下文或未关闭的线程池间接持有,就无法回收 -
该类对应的 Class 对象无任何强引用:常见泄漏点包括
static Map<string class></string>缓存、ThreadLocal<class></class>未调用remove()、反射调用后遗留的Method.setAccessible(true)、JNI 全局引用(NewGlobalRef未配对DeleteGlobalRef)
清理实际发生在 Full GC 阶段
即使三个条件都满足,JVM 也不会立刻卸载——它只在执行 Full GC(如 G1 的并发标记完成阶段、CMS 的 remark 后)时扫描可卸载类。此时:
- JVM 遍历所有不可达的 ClassLoader 实例,对其加载的每个类逐一验证上述三条件
- 通过验证的类,其元数据(方法签名、字段、常量池、注解等)从 Metaspace 中移除,对应内存块进入内部空闲池
- 内存不会立即还给操作系统,仅当空闲率超过
-XX:MaxMetaspaceFreeRatio(默认 70%)且有大块连续空间时,才尝试收缩并返还 OS
自定义类加载器是唯一可行的突破口
系统类加载器(AppClassLoader、Bootstrap ClassLoader)生命周期与 JVM 一致,永远不会被回收;真正能触发卸载的,只有动态场景中的自定义加载器:
- Tomcat 的
WebAppClassLoader在应用停用时,若监听器未释放、线程池未关闭,就会持续持有所加载类 - OSGi 或 Spring Boot DevTools 热加载失败时,新旧 ClassLoader 切换卡住,旧加载器残留导致整批类堆积
- 手动使用
URLClassLoader加载插件后,必须显式置空所有强引用,并调用Thread.currentThread().setContextClassLoader(null)
如何确认卸载真的发生了
不能只看 jstat -gc 的 MU(Metaspace used)下降——那可能是类加载失败回滚,而非卸载成功。可靠方式只有两种:
- 启动参数加
-XX:+TraceClassUnloading,日志中出现类似[Unloading class com.example.PluginService]才算实锤 - 用
jcmd <pid> VM.native_memory summary scale=MB</pid>对比前后 “class” 区域变化,再结合jmap -histo:live确认该类实例数为 0
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











