核心是确认类加载器存活状态及强引用链是否阻断卸载:通过-xx:+traceclassloading/unloading和gc日志观察加载卸载比例,用mat分析堆转储中可疑类加载器及其gc roots引用链,最终通过调参与代码修复(如tccl恢复、代理缓存)切断泄漏源。

核心在于确认类加载器是否存活、它加载的类是否被强引用、以及这些引用链是否阻断了卸载路径。类加载器活着,它加载的所有类元数据就一直占着元空间不放。
看日志:确认类有没有真正卸载
启动时加上这几个参数,让 JVM 把加载和卸载行为打出来:
- -XX:+TraceClassLoading —— 打印每个类从哪加载、何时加载
- -XX:+TraceClassUnloading —— 只有这个开了,你才能看到“谁被卸载了”
- -Xlog:gc+metaspace*=debug(JDK 10+)或 -XX:+PrintGCDetails(老版本)—— 查看元空间使用量变化
重点观察:重启/热部署后,加载次数大幅上升但卸载为 0,基本就能锁定是类加载器没被回收,导致类堆积。
查堆转储:定位“活着的类加载器”
发生 Metaspace OOM 后,JVM 通常会自动生成 .hprof 文件(或用 jmap -dump:format=b,file=meta.hprof
- 点菜单 → Class Loader Explorer,直接列出所有类加载器实例
- 找名字含 RestartClassLoader、WebAppClassLoader、DevToolsRestartClassLoader 这类明显和热部署相关的加载器
- 看它们的 “Loaded Classes” 数量和 “Retained Heap” 大小——如果多个实例都“活着”,且每个都占几十 MB,就是泄漏源
挖引用链:找到谁在拽着类加载器不放
在 MAT 中右键某个可疑的类加载器实例 → Path to GC Roots → 选 exclude weak/soft references:
- 常见阻断点:线程上下文 ClassLoader(TCCL) 被线程池长期持有
- 静态变量(比如 public static Map
CACHE )里存了某个类的实例,而该实例又持有了 Class 或 ClassLoader 引用 - 第三方库(如 CGLIB、Javassist 生成的代理类)把 ClassLoader 存在了内部缓存中,且未清理
顺着引用链一层层点进去,最终会落到某个业务代码、配置类或框架组件上——那就是修复入口。
验证与收口:加参数 + 改代码双管齐下
排查出问题后,不能只靠调参扛着,得切断泄漏源头:
- 对热部署场景(如 DevTools),加参数 -XX:+CMSClassUnloadingEnabled(CMS)或 -XX:+ZUnloading(ZGC),并确保 -XX:+UseConcMarkSweepGC 或对应 GC 器启用
- 手动管理 TCCL 的地方,必须用 try-finally 恢复原始 ClassLoader:Thread.currentThread().setContextClassLoader(original)
- 动态代理类尽量复用,避免每次请求都 new 一个 CGLIB Enhancer;用 Enhancer.setUseCache(true)
- 限制元空间上限防雪崩:-XX:MaxMetaspaceSize=256m(根据实际调整)










