类加载器泄漏导致Metaspace OOM,本质是旧ClassLoader被强引用无法回收,致其加载的类元数据持续堆积;需通过日志追踪加载/卸载、jstat监控类数量、MAT分析GC Roots定位静态缓存/ThreadLocal/TCCL等持有源,并修复代码而非仅调参。

类加载器引发的内存溢出,核心表现是 java.lang.OutOfMemoryError: Metaspace(JDK 8+)或旧版的 java.lang.OutOfMemoryError: Compressed class space / PermGen space。它不是堆内存不够,而是 JVM 用来存放类元数据(类名、方法签名、常量池、注解等)的本地内存区域被耗尽。根本原因往往是类被反复加载却无法卸载——而类卸载的前提,是其对应的 ClassLoader 实例被 GC 回收。一旦类加载器被意外持有,它加载的所有类就永远驻留,Metaspace 持续膨胀直至 OOM。
看日志 + 查参数,确认是不是类加载器问题
先盯紧错误信息和启动配置:
- 报错必须含
Metaspace或Compressed class space,排除堆、直接内存等干扰项 - 用
jinfo -flags <pid></pid>确认是否设置了-XX:MaxMetaspaceSize。没设则默认无上限,容易掩盖泄漏;设了但值过小(如64m),可能只是配置不合理 - 检查是否启用了热部署工具(Spring Boot DevTools、JRebel)、OSGi 容器、或自定义 ClassLoader 的框架(如某些插件化系统)
抓线索:从类加载行为和内存分布入手
别等 OOM 才行动,日常就要监控类加载动态:
当用户要求为 Java 代码添加注释、修改注释、审查注释、整理注释格式,或要求按照代码注释规范处理 Java 文件时,使用本技能。本技能用于统一生成和维护 Java 类注释模板、按 Git 对比 release 判断版本号、补充每行代码上方中文块注释、调整注释空行规则,并保持 Java 文件 UTF-8 无 BOM 编码。典型触发场景包括:补充类注释、更新 @version、检查 @author 来源、整理行尾注释、把注释放到代码上一行、检查注释是否符合规范、构建前检查 Java 注释风格。
- 加 JVM 参数开启类加载日志:
-verbose:class或更结构化的-Xlog:class+load=debug(JDK 10+)。观察是否有大量重复类名被加载(如com.example.Service$$EnhancerBySpringCGLIB$$xxx不断变号) - 用
jstat -class <pid></pid>实时看类加载总数(loaded)和卸载数(unloaded)。若loaded持续上涨、unloaded几乎为 0,就是危险信号 - 启用
-XX:+PrintGCDetails并关注 GC 日志中 Metaspace 区域的回收行为。如果频繁触发 Metaspace GC 却几乎不释放空间,说明类加载器卡住了
定位泄漏根源:谁在强持有 ClassLoader?
Metaspace 泄漏本质是 ClassLoader 泄漏。分析关键在于找出“不该活的 ClassLoader 实例”及其强引用链:
立即学习“Java免费学习笔记(深入)”;
- 用
jmap -dump:format=b,file=meta_dump.hprof <pid></pid>抓堆快照(OOM 前或内存持续上涨时) - 用 MAT(Memory Analyzer Tool)打开,执行 Leak Suspects Report,它常会直接标出可疑的 ClassLoader 实例
- 切换到 Dominator Tree,按 Retained Heap 排序,找
java.net.URLClassLoader、org.springframework.boot.devtools.restart.classloader.RestartClassLoader等非系统类加载器 - 右键该 ClassLoader → Path to GC Roots → 勾选 exclude all weak/soft references。终点大概率是:
• 静态字段(如static Map<string classloader></string>)
• ThreadLocal 变量(尤其 Web 容器中未清理的请求上下文)
• 某个长期存活的业务对象(如缓存、监听器、单例服务)隐式持有 ClassLoader 引用
常见场景与修复方向
以下模式高频导致 ClassLoader 泄漏:
- Spring AOP + CGLIB 动态代理:每次生成新代理类,若代理类被静态缓存或跨 ClassLoader 使用,旧 ClassLoader 就无法卸载。解决:避免在静态集合中缓存代理对象;升级 Spring 到支持代理类复用的版本
-
热部署未清理:DevTools 在重启时未彻底销毁旧 ClassLoader。解决:生产环境禁用 DevTools;排查自定义热部署逻辑中是否遗漏
ClassLoader.close()或资源清理 -
线程上下文 ClassLoader(TCCL)滥用:线程池中线程的 TCCL 被设为某个临时 ClassLoader 后未重置,导致该 ClassLoader 被线程长期持有。解决:在任务执行前后显式保存并恢复 TCCL,或使用
Thread.setContextClassLoader(null) - 第三方 SDK 隐式注册:某些日志、监控、序列化 SDK 会在初始化时将自身 ClassLoader 注册到全局静态容器。解决:查阅 SDK 文档确认清理方式;必要时通过反射手动注销

















