ClassLoader泄漏导致Metaspace持续增长、卸载为零、最终OOM,根源是类加载器被强引用无法退出,致使所加载类元数据无法回收;需通过-XX:+TraceClassLoading/Unloading日志、jcmd/MAT分析Loaded Classes及GC Roots定位“活尸”ClassLoader,并修复TCCL、静态持有和动态代理问题。

ClassLoader 泄漏直接锁住元空间里所有已加载类的元数据,导致 Metaspace 持续增长、卸载为零、最终 OOM。问题不在于“类太多”,而在于“类加载器退不出”——只要它被强引用挂着,整条类树就无法回收。
看日志:确认卸载是否真的发生
启动时加上关键参数,让 JVM 主动说话:
- -XX:+TraceClassLoading:记录每个类从哪个 ClassLoader 加载、何时加载
- -XX:+TraceClassUnloading:只有开启这个,才能看到 “Unloading class xxx” 日志;没这条,基本等于没卸载
- -Xlog:gc+metaspace*=debug(JDK 10+)或 -XX:+PrintGCDetails(老版本):观察 Metaspace used 是否只涨不跌、Full GC 后几乎无释放
重点盯热部署或重启后:加载数猛增,卸载数长期为 0 —— 这是 ClassLoader 卡死最直接的信号。
抓堆转储:定位“活尸”类加载器
OOM 后 JVM 通常自动生成 .hprof(或用 jmap -dump:live,format=b,file=heap.hprof <pid> 手动抓),用 Eclipse MAT 分析:
- 打开 → Class Loader Explorer,按 Retained Heap 排序
- 找名字含 WebAppClassLoader、RestartClassLoader、DevToolsRestartClassLoader 的实例
- 点开看 Loaded Classes 数量(几万?)、Retained Heap 大小(几十 MB?)、是否多个实例长期共存
这些就是“活尸”——活着但不该活,占着元空间不放。
挖 GC Roots:找到拽住它的那只手
对可疑 ClassLoader 右键 → Path to GC Roots → exclude weak/soft references,常见阻断点有三类:
- 线程池 Worker 线程:长期持有 Thread Context ClassLoader(TCCL),任务执行完也没还原
- 静态缓存:public static Map<?, ?> CACHE 存了某个类实例,而该实例内部持有了 Class 或 ClassLoader 引用
- 第三方库缓存:CGLIB/Javassist/ORM/AOP 框架把 ClassLoader 存进内部 Map,且未提供清理入口
顺着引用链一层层点进去,最终会落到某行业务代码、某个配置类,或某段未关闭的代理封装逻辑上。
验证与修复:别只调参,要切断链
临时加大 -XX:MaxMetaspaceSize 只是延缓崩溃,真正要做的有三件事:
- TCCL 必须显式恢复:在线程任务开始前设为 null 或 AppClassLoader,结束时 try-finally 还原原始值
- 清理静态持有:检查所有 public static 集合、单例、监听器,确保不长期持有 ClassLoader 或其加载的类实例
- 约束动态类生成:禁止在高频路径中反复 new Proxy / cglib.Enhancer;改用启动预生成 + ConcurrentHashMap 缓存复用
不复杂但容易忽略。


















