class能被成功卸载需同时满足三个条件:该类所有实例已被回收、其classloader实例已被回收、该类的class对象无任何引用;仅当full gc触发且三者全满足时,metaspace元数据才可能被释放。

不能靠“流程控制语句”判定或剔除类元数据残留。Java 中没有 if/while/for 这类语法能直接触发类卸载或检测 Metaspace 状态。所谓“判定并剔除”,本质是验证卸载条件是否满足,并通过代码干预让 JVM 有机会回收——关键在引用管理与可观测性,不在控制流逻辑。
用可观测日志确认卸载是否发生
启动 JVM 时加上:
-Xlog:class+unload=info
运行热更新后观察控制台或日志文件:出现 Unloading class com.example.Xxx 才说明卸载真正执行了。没这条日志,代表 ClassLoader 还活着,所有流程控制都无效。
切断三类强引用链(必须手动做)
ClassLoader 不被回收,元数据就永远卡在 Metaspace 里。重点清理:
- 静态持有:清空 static Map、关闭 static ExecutorService、释放 static Logger 的上下文
- 线程上下文类加载器(TCCL):在异步任务、Filter、ThreadLocal 初始化前保存旧 TCCL,退出时 set 回去
- 第三方库绑定:调用 Log4j2 的 LoggerContext.stop()、Jackson 的 ObjectMapper.reset()、Hibernate SessionFactory.close()
每次热更新都新建独立 ClassLoader
不要复用、不要缓存、不要继承 AppClassLoader:
- 构造时传 null 作为 parent:
new URLClassLoader(urls, null) - 只覆盖 findClass(),不重写 loadClass(),避免破坏双亲委派
- 加载完成后,旧 loader 实例立即置为 null,且不存入任何全局容器
用 jstat 和 jcmd 验证效果
别信代码“执行完了”,要看真实指标:
-
jstat -gc <pid></pid>:关注 MCL(已加载类数)是否下降、MCU(已卸载类数)是否增长 -
jcmd <pid> VM.classloader_stats</pid>:检查某类加载器实例数是否持续上涨但单个加载类数极少——这是泄漏铁证











