类加载器泄漏本质是类加载器实例无法被垃圾回收,导致元空间内存溢出;主因是强引用链阻止卸载,如TCCL未恢复、静态缓存持实例、CGLIB缓存ClassLoader、ThreadLocal持有对象;需通过日志、堆转储、GC Roots分析定位并切断引用链。

类加载器泄漏本质是类加载器实例无法被垃圾回收,导致它加载的所有类元数据长期滞留在元空间(Metaspace),最终引发 java.lang.OutOfMemoryError: Metaspace。关键不在“类没卸载”,而在于“加载器还活着”,且被某条强引用链牢牢拽住。
常见泄漏原因
真正卡住类加载器回收的,往往不是业务代码直白地持有引用,而是几类隐蔽但高频的模式:
- 线程上下文类加载器(TCCL)未恢复:线程池中线程复用时,若某次任务临时设置了 TCCL(如加载插件、执行脚本),却未在 finally 块中 restore 原始 ClassLoader,后续所有任务都会继承该 TCCL,使对应加载器无法回收
-
静态缓存持有类实例或 Class 对象:例如
public static Map<String, SomeService> CACHE中存入了由特定 ClassLoader 加载的SomeService实例;该实例内部可能隐式持有this.getClass().getClassLoader(),形成闭环引用 -
第三方代理库缓存 ClassLoader:CGLIB、Javassist 在生成动态代理类时,常将 ClassLoader 存入自身静态缓存(如
Enhancer.registerCallbacks或内部 Map),且未提供清理机制,尤其在热部署场景下反复触发,缓存越积越多 - ThreadLocal 持有加载器相关对象:自定义 ThreadLocal 变量存储了由特定 ClassLoader 加载的类实例,或直接存了 ClassLoader 本身;线程不销毁,ThreadLocal 的 value 就一直存在,阻断卸载路径
诊断三步法:日志 → 堆转储 → 引用链
不能只靠猜,得用证据链闭环验证:
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
-
加参数看行为:启动时加上
-XX:+TraceClassLoading -XX:+TraceClassUnloading -Xlog:gc+metaspace*=debug(JDK 10+)或-XX:+PrintGCDetails(旧版)。重点观察热部署后:加载类数飙升,但Unloading class xxx日志几乎为零——说明类根本没卸载,根源大概率是加载器没死 -
抓堆转储定位加载器:发生 Metaspace OOM 后,JVM 通常自动生成
.hprof;或手动执行jmap -dump:format=b,file=meta.hprof <pid>。用 MAT 打开,进入 Class Loader Explorer,筛选名称含WebAppClassLoader、RestartClassLoader、LaunchedURLClassLoader的实例,重点关注 “Retained Heap” 大小和 “Loaded Classes” 数量异常偏高的多个存活实例 -
挖 GC Roots 断点:在 MAT 中右键可疑 ClassLoader → Path to GC Roots → exclude weak/soft references。常见终点包括:
java.util.concurrent.ThreadPoolExecutor$Worker(线程池持 TCCL)、static final CACHE(静态 Map)、net.sf.cglib.core.DefaultGeneratorStrategy(CGLIB 缓存)、java.lang.ThreadLocal$ThreadLocalMap(ThreadLocal value)
修复核心:切断强引用,而非调大参数
单纯加大 -XX:MaxMetaspaceSize 只是拖延时间。真正修复需从代码层切断泄漏源头:
立即学习“Java免费学习笔记(深入)”;
- 所有显式设置 TCCL 的地方,必须配对使用 try-finally:
ClassLoader orig = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader(pluginCl); ... } finally { Thread.currentThread().setContextClassLoader(orig); } - 静态缓存中避免存具体类实例;改用 WeakReference 包装,或确保在应用卸载时主动清空(如 Spring 的
@PreDestroy、Servlet 的contextDestroyed) - 排查 CGLIB/Javassist 使用点,升级到支持自动清理的版本(如 CGLIB 3.3.0+),或在代理创建后手动调用其清理方法(如
Enhancer.clearCache()) - ThreadLocal 使用后务必调用
remove(),尤其在框架回调、Filter、Interceptor 等生命周期不确定的场景
元空间泄漏不像堆内存那样容易被 GC 干扰,它依赖类加载器的彻底死亡。只要找到那根“拽着加载器不撒手”的引用链,问题就解决了一大半。

















