类加载器内存泄漏表现为Metaspace持续增长且Full GC无法回收,根源是ClassLoader被强引用导致其加载的类无法卸载;需通过jcmd、jmap+MAT等工具定位持有链,并清理静态引用、ThreadLocal、钩子等常见泄漏点。

类加载器内存泄漏是 Java 应用中较隐蔽但后果严重的问题,尤其在热部署、OSGi、Spring Boot 多模块或频繁动态生成类(如 CGLib 代理、JSON 反序列化、模板引擎)的场景下极易发生。它不表现为堆内存持续增长,而是元空间(Metaspace)不断膨胀、类数量激增、最终 java.lang.OutOfMemoryError: Metaspace,且 Full GC 无法回收。
看现象:先确认是不是类加载器泄漏
满足以下多个特征时,高度怀疑类加载器泄漏:
- 应用运行一段时间后,Metaspace 使用量持续上升,即使多次 Full GC 也不回落;
-
jstat -gc <pid>显示MU(Metaspace Used)稳步上涨,MC(Metaspace Capacity)也同步扩大; -
jcmd <pid> VM.native_memory summary或jstat -compiler <pid>显示已加载类总数(loaded)持续增加,卸载数(unloaded)极少或为 0; - 重启服务后 Metaspace 占用重置,但数小时/天内再次快速打满;
- 日志中出现
java.lang.OutOfMemoryError: Metaspace,而非Java heap space。
查根源:定位泄漏的类加载器和类
核心逻辑是:类能否被卸载,取决于其对应的 ClassLoader 是否还存活。只要 ClassLoader 被强引用持有,它加载的所有类及其元数据就无法从 Metaspace 中释放。
常用排查手段:
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
立即学习“Java免费学习笔记(深入)”;
-
导出类直方图:
jcmd <pid> VM.class_histogram,重点关注java.net.URLClassLoader、org.springframework.boot.loader.LaunchedURLClassLoader、WebAppClassLoader等非系统类加载器的实例数量; -
查看类加载器统计:
jcmd <pid> VM.classloader_stats,输出各 ClassLoader 类型的加载/卸载数量,若某类加载器卸载数为 0 且实例数只增不减,即为嫌疑对象; -
生成堆转储分析 ClassLoader 引用链:用
jmap -dump:live,format=b,file=heap.hprof <pid>获取 dump,再用 MAT 打开 → “Histogram” → 搜索ClassLoader→ 右键某个可疑 ClassLoader 实例 → “Merge Shortest Paths to GC Roots”(排除弱/软引用),查看谁在长期持有它; -
启用类加载日志:JVM 启动加参数
-XX:+TraceClassLoading -XX:+TraceClassUnloading,观察是否有大量Loaded但几乎无Unloaded的记录。
盯常见泄漏点
以下代码模式极易导致 ClassLoader 泄漏:
-
静态持有 WebAppClassLoader:例如工具类中
static ClassLoader cl = Thread.currentThread().getContextClassLoader();,而该上下文 ClassLoader 是 Tomcat 的WebAppClassLoader,生命周期本应随应用结束,却被静态变量拖住; -
ThreadLocal 存储 ClassLoader 或其加载的类实例:线程池复用线程时,ThreadLocal 若未
remove(),会持续引用旧 ClassLoader; - JNI 或 NIO Direct Buffer 关联 ClassLoader:某些 native 库在初始化时缓存了 ClassLoader,未提供清理接口;
-
动态代理/字节码增强未清理:CGLib
Enhancer、Javassist 创建的代理类,若缓存了ClassLoader或生成的Class对象未释放,也会卡住整个加载器; -
监听器/钩子未注销:如
Runtime.addShutdownHook()中传入了由业务 ClassLoader 加载的 Runnable,ShutdownHook 线程会持有该 ClassLoader。
验证与修复
修复后务必验证是否真正解决:
- 重启应用,用
jstat -gc <pid> 2s持续观察 Metaspace 使用趋势,应保持平稳或仅缓慢小幅上升; - 执行一次完整应用生命周期(如热部署一次、或模拟一次模块重载),再检查
jcmd <pid> VM.classloader_stats,确认卸载数明显增加; - 避免简单调大
-XX:MaxMetaspaceSize,这只是掩盖问题,不能根治; - 关键原则:所有对 ClassLoader 的引用都应是局部、短期、可清除的;涉及线程、静态缓存、全局钩子的地方,必须显式清理。

















