类加载器未释放导致内存泄漏的关键是classloader实例被强引用钉住,致其加载的类及元数据滞留metaspace,mu持续上涨;需结合热部署后mu/mc逐次上升、full gc不回落、动态类加载行为、metaspace oom等现象综合判断。

识别类加载器未释放导致的内存泄漏,关键不是看“类有没有卸载”,而是看“类加载器本身是否还被强引用钉住”。只要 ClassLoader 实例还活着,它加载的所有类、静态变量、方法元数据就一直卡在 Metaspace 里,MU(Metaspace Used)就会持续上涨。
结合运行时行为特征判断
仅看 jstat 或堆内存高不够,需叠加以下现象才高度可疑:
- 应用多次热部署或重启后,MU 和 MC 都逐次上升,且 Full GC 后不回落(用
jstat -gc <pid></pid>持续观察) - 存在动态类加载行为:如 Spring Boot DevTools、JSP 容器、OSGi、自定义 ClassLoader、插件化框架
- 日志中频繁出现
Full GC (Metadata GC Threshold),但 Metaspace 使用量未明显下降 - JVM 启用了
-XX:MaxMetaspaceSize,却仍报java.lang.OutOfMemoryError: Metaspace
从堆转储中定位可疑类加载器
用 jmap -dump:format=b,file=heap.hprof <pid></pid> 获取 dump 后,用 MAT 分析:
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
- 打开 Leak Suspects Report——MAT 常自动标出被强引用的 ClassLoader 实例
- 执行 OQL:
SELECT * FROM java.lang.ClassLoader,按 Retained Heap 或 Objects 数量排序,重点关注非AppClassLoader、PlatformClassLoader的实例(如WebAppClassLoader、LaunchedURLClassLoader) - 对高 Retained Heap 的 ClassLoader 右键 → Path to GC Roots(勾选 “exclude weak/soft references”),重点看谁在强引用它
盯紧三类典型强引用锚点
90% 以上的类加载器泄漏,都源于以下三处之一没清理:
-
静态集合持有 Class 或 ClassLoader:比如
private static Map<string class> cache = new HashMap()</string>—— 改用WeakHashMap,或在销毁阶段主动clear() -
ThreadLocal 未 remove():线程池复用时,若业务往 ThreadLocal 存了当前上下文类加载器加载的对象,又没在
finally或拦截器afterCompletion中remove(),该线程就长期持有了旧 ClassLoader -
守护线程未重置 ContextClassLoader:自启线程(如定时上报、心跳检测)创建时继承了父线程的 context class loader;若未在
run()开头调用Thread.currentThread().setContextClassLoader(null),线程生命周期就等于 ClassLoader 生命周期
验证是否真卸载失败
类卸载必须同时满足三个条件,缺一不可:
- 该类所有实例已被 GC 回收
- 加载它的 ClassLoader 实例本身可被 GC(无强引用)
- 该类的
java.lang.Class对象也无强引用(包括静态字段、JNI 全局引用、未清理的 ThreadLocal 值等)
只要其中任一条件不满足,类元数据就永远留在 Metaspace,泄漏就成立。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










