java类卸载需同时满足三条件:所有实例被回收、classloader被回收、class对象无任何引用;否则metaspace中类元数据将持续残留,引发outofmemoryerror: metaspace。

Java 中没有 System 类的公开方法能直接“判定”或“剔除”类元数据残留。所谓“类元数据残留”,本质是旧 ClassLoader 及其所加载类未被回收,导致 Metaspace 占用持续增长、热更新后行为异常。真正有效的操作不在 System,而在切断引用链、触发卸载条件,并借助 JVM 诊断能力验证效果。
识别元数据是否真正释放
不能靠猜测,要靠可观测证据:
- 启动时添加 JVM 参数:
-Xlog:class+unload=info,观察日志中是否出现类似Unloading class com.example.ServiceImpl (0x00000008002a1234)的记录;无此类日志,说明卸载根本未发生 - 定期执行
jstat -gc <pid></pid>,重点关注MC(Metaspace 容量)、MU(已使用量)和MCL(已加载类数)、MCU(已卸载类数);若MCU长期为 0 或极低,说明卸载逻辑失效 - 用
jcmd <pid> VM.classloader_stats</pid>查看各 ClassLoader 实例数量及其加载类数,发现某类加载器实例数持续上涨但单个加载类数极少,就是典型的泄漏信号
清理前必须切断的三类强引用
ClassLoader 被 GC 的前提,是它不再被任何 GC Root 强引用。常见泄漏点需逐项清理:
-
静态持有:检查所有
static final Map<string class></string>、static Logger、static ExecutorService,在热更新前调用clear()、shutdownNow()或close() -
线程上下文类加载器:异步任务、Filter、ThreadLocal 初始化等场景中,若调用了
Thread.currentThread().setContextClassLoader(customLoader),必须在退出前恢复原 loader(如保存旧值并set回去) -
第三方库隐式绑定:Log4j2 的
LoggerContext、Jackson 的SimpleModule、Hibernate 的SessionFactory等,常通过 static 持有当前 ClassLoader,需调用其stop()、close()或启用隔离配置(如 Jackson 设置MapperFeature.USE_STATIC_TYPING)
ClassLoader 构建与销毁的关键实践
不是“销毁动作”起作用,而是让旧 loader 彻底不可达:
- 新建时断开父委托:
new URLClassLoader(urls, null),避免继承 AppClassLoader 导致旧类混杂、引用链无法切断 - 不重写
loadClass(),只覆盖findClass(),确保双亲委派不失控,基础类(如java.lang.Object)仍由 Bootstrap 加载 - 每次热更新都新建 ClassLoader 实例,旧实例在完成清理后立即置为
null,且不再存入任何全局容器或缓存 - 加载类后,仅通过反射创建实例(如
clazz.getDeclaredConstructor().newInstance()),避免编译期类型引用造成隐式持旧 loader
配合容器与 JVM 验证卸载真实发生
代码清理只是必要条件,不是充分条件:
- Tomcat 中确认
context.xml启用reloadable="true",并关闭 JSP 编译缓存(development="false"),否则.jasper目录残留会拖住 loader - WebLogic 控制台将“旧版本保留数”设为 0,“重新部署超时”设为 30 秒以上,防止旧部署因等待资源而长期挂起
- 添加
-XX:+UnlockDiagnosticVMOptions -XX:+PrintClassHistogramAfterFullGC,在 Full GC 后打印类直方图,对比前后java.lang.Class和自定义类实例数是否下降










