classloader未被回收会导致其加载的class无法卸载。需检查静态缓存、线程上下文、框架持有、日志/序列化工具引用,以及class对象残留引用;通过jvm参数和工具验证卸载行为,并主动切断引用链。

排查因误用静态长生命周期 ClassLoader 导致的 Class 无法卸载,核心是验证“ClassLoader 是否真被回收”——因为只要它还活着,它加载的所有类就全卡在 Metaspace 里,不管实例有没有、Class 有没有引用。
确认 ClassLoader 是否仍被强引用持有
静态字段、线程上下文、第三方框架缓存是最常见的“隐形锁”。重点检查:
- 是否有 static final ClassLoader 或 static Map
缓存了自定义加载器 - 是否调用过 Thread.currentThread().setContextClassLoader(myLoader) 却未在 finally 中重置为 null 或原始值
- Spring 上下文是否通过 ContextClassLoader 持有旧加载器(常见于 DevTools 热部署后未清理)
- 日志框架(如 Logback)、序列化工具(如 Kryo)、反射工具类是否缓存了该加载器或其加载的 Class
验证 Class 对象是否还有残留引用
即使 ClassLoader 看似可回收,只要它的 Class 对象还被某处拿着,卸载就失败。检查以下高频泄漏点:
-
static Map
:尤其插件注册表、Groovy 脚本缓存中直接 put(Class) 的场景 -
ThreadLocal
或 ThreadLocal 中存了该类实例且未 remove() - 反射操作后遗留的 Method/Field 引用(
setAccessible(true)本身不阻止卸载,但缓存 Method 就会) - JNI 全局引用:
NewGlobalRef(env, clazz)调用后未配对DeleteGlobalRef
用 JVM 参数和工具定位真实行为
别靠猜,用日志和指标说话:
- 启动时加 -XX:+TraceClassUnloading -verbose:class,观察日志中是否出现
[Unloading class xxx]—— 没这行,基本说明加载器没死 - 运行中执行 jcmd
VM.native_memory summary 或 jstat -gcmetacapacity,看 Metaspace used 是否持续上涨且 MC(Metaspace capacity)不断扩容 - 用 jmap -clstats
查看各 ClassLoader 实例数量及加载类数,对比预期与实际是否异常增长 - 触发一次 Full GC 后,再查 jstat -gc
,观察 MU(Metaspace used)是否下降;若不变,说明类根本没卸
修复与预防的关键动作
不是等 GC,而是主动切断引用链:
- 动态加载器用完后,显式置空所有强引用:
myClassLoader = null;,并确保无静态集合缓存它 - 在线程池任务结束前,强制重置上下文类加载器:
Thread.currentThread().setContextClassLoader(null) - 改用 WeakReference
缓存(仅适用于非关键路径),避免长期持有可能导致泄漏的引用 - 避免在静态初始化块中 new 自定义 ClassLoader;改用懒加载 + 显式销毁接口











