自定义类加载器卸载失败的核心是classloader实例被强引用链持住而无法回收,导致其加载的类、静态字段及元数据滞留metaspace;需通过-xx:+traceclassloading/-xx:+traceclassunloading/-xlog:gc+metaspace*=debug观察卸载日志,jmap抓堆转储后用mat按class loader explorer筛选自定义加载器并path to gc roots定位静态缓存、threadlocal、守护线程tccl或jni全局引用等锚点,再在销毁阶段主动清理tccl、改用weakhashmap、clear缓存、关闭线程池。

自定义类加载器卸载失败的泄漏,核心不是“类没卸载”,而是你写的那个 ClassLoader 子类实例一直活着,被某条强引用链死死拽住——它一活,所有它加载的类、静态字段、方法元数据就全卡在 Metaspace 里出不来。
看日志确认卸载行为是否真实发生
先让 JVM 把话说明白,启动时加这三组参数:
-
-XX:+TraceClassLoading:看到每个类由哪个加载器加载,尤其关注你的自定义类加载器名(比如
MyPluginClassLoader)是否高频出现 -
-XX:+TraceClassUnloading:必须开启!否则根本看不到 “
com.example.MyService class unloaded” 这类关键日志 -
-Xlog:gc+metaspace*=debug(JDK 10+)或 -XX:+PrintGCDetails(旧版):重点观察每次 Full GC 后 Metaspace 使用量是否回落;如果
Metaspace: xxxK->xxxK箭头后几乎不变,说明卸载基本没生效
热部署或插件重载后,若加载日志猛增、卸载日志为零或个位数,基本可断定:你的自定义加载器没被回收。
抓堆转储定位“活着的自定义加载器”
在疑似泄漏时手动抓一份堆转储:
Java开发手册规约集合,基于阿里巴巴Java开发手册(嵩山版)。 涵盖7大维度:编程规约、异常日志、单元测试、安全规约、MySQL数据库、工程结构、设计规约。 当用户需要:(1) 编写或审查Java代码 (2) 检查命名/代码规范 (3) 处理异常和日志 (4) 编写单元测试 (5) 安全编码 (6) 数据库设...
- 运行中执行:
jmap -dump:format=b,file=customcl.hprof <pid></pid> - 用 Eclipse MAT 打开 → 菜单栏 Class Loader Explorer → 按 Loaded Classes 或 Retained Heap 排序
- 重点筛选名称含
MyPluginClassLoader、DynamicModuleClassLoader等你自定义的类名,而不是系统默认的AppClassLoader
如果发现多个实例都“存活”,且每个都加载了上百甚至上千个类、Retained Heap 达几 MB,就是明确泄漏信号。
顺着 GC Roots 找谁在持有着加载器
在 MAT 中右键某个可疑的自定义 ClassLoader 实例 → Path to GC Roots → exclude weak/soft references:
- 常见锚点一:**静态缓存持有其加载的类实例**,例如
public static Map<string pluginservice> PLUGIN_CACHE = new HashMap();</string>—— 这个PluginService实例内部隐式持有this.getClass().getClassLoader() - 常见锚点二:**ThreadLocal 未清理**,尤其在复用线程池中设置过
Thread.currentThread().setContextClassLoader(myCl)却没在finally中恢复或remove() - 常见锚点三:**守护线程绑定 TCCL**,比如你启了一个后台上报线程,在
run()开头没重置Thread.currentThread().setContextClassLoader(null),该线程生命周期就等于你的加载器生命周期 - 常见锚点四:**JNI 全局引用未释放**,若通过 JNI 加载类并调用了
NewGlobalRef,必须配对DeleteGlobalRef,否则 native 层直接钉死 Java 层加载器
代码层修复关键动作
不能只靠 GC 等着回收,得主动切断引用:
- 所有设置 TCCL 的地方,必须用
try-finally包裹,并在 finally 中恢复原始 ClassLoader 或设为null - 静态缓存改用
WeakHashMap<string weakreference>></string>,或在插件卸载逻辑中显式CACHE.clear() - 实现
AutoCloseable或监听容器销毁事件(如ServletContextListener.contextDestroyed()),在其中遍历清理 ThreadLocal、注销监听器、关闭线程池 - 动态代理场景慎用 CGLIB/Javassist,若必须用,确认其缓存机制是否支持按 ClassLoader 隔离或提供清理入口;否则考虑改用 JDK 动态代理(基于接口,不生成新类)
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










