java类加载器泄露本质是旧classloader无法被gc回收,导致其加载的类、静态变量和元数据长期驻留内存,引发metaspace持续增长乃至oom;排查关键在于定位强引用classloader的对象,通过日志追踪类加载/卸载、堆转储分析classloader实例及引用链,并结合参数调优与代码修复(如tccl恢复、线程关闭)双线治理。

Java 应用类加载器泄露本质是:旧的 ClassLoader 实例无法被 GC 回收,导致它加载的所有类、静态变量、元数据长期驻留内存,最终引发 Metaspace 持续增长甚至 OOM。排查关键不在于“找对象”,而在于“找谁拽着 ClassLoader 不放”。
看日志:确认类是否真正卸载
启动 JVM 时加上以下参数,让运行时主动“说话”:
-
-XX:+TraceClassLoading:记录每个类从哪个 ClassLoader 加载、何时加载 -
-XX:+TraceClassUnloading:只有开启此项,才能看到“XXX class unloaded”这类日志 -
-Xlog:gc+metaspace*=debug(JDK 10+)或-XX:+PrintGCDetails(老版本):观察 Metaspace 使用量与 GC 行为
重点看热部署或重启后:类加载数量大幅上升,但 unloaded 日志极少或为零——这说明 ClassLoader 没被回收,类自然也无法卸载。
抓堆转储:定位“活着的可疑加载器”
发生 Metaspace OOM 或怀疑泄漏时,生成堆转储:
- 自动触发:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump.hprof - 手动抓取:
jmap -dump:format=b,file=classes.hprof <pid></pid>
用 Eclipse MAT 打开后:
- 菜单 → Class Loader Explorer,直接列出所有 ClassLoader 实例
- 重点关注名称含
WebAppClassLoader、RestartClassLoader、DevToolsRestartClassLoader的实例 - 检查它们的 Loaded Classes 数量 和 Retained Heap 大小:多个实例同时“存活”且各占几十 MB,基本就是泄漏源
挖引用链:找到阻断 GC 的根因
在 MAT 中右键某个可疑 ClassLoader → Path to GC Roots → 勾选 exclude weak/soft references:
-
线程上下文 ClassLoader(TCCL)被长期持有:如 Netty 的
GlobalEventExecutor启动线程时未恢复原始 TCCL,导致线程一直持有所属 Web 应用的 ClassLoader -
静态缓存持有业务类实例:例如
public static Map<string someservice> CACHE</string>存了某个由 Web 类加载器加载的 Service 实例,该实例又隐式持有其 Class 和 ClassLoader - 第三方代理库缓存 ClassLoader:CGLIB、Javassist 生成代理类时,常将 ClassLoader 缓存在静态 Map 中,且未随应用卸载清理
顺着引用链逐层下钻,最终会落到某行代码、某个配置类或某个框架组件上——那就是修复点。
验证与收口:参数 + 代码双线并进
仅靠调参不能根治,必须切断泄漏源头:
- 对热部署场景(如 Spring Boot DevTools),启用类卸载支持:
-XX:+CMSClassUnloadingEnabled(CMS)或-XX:+ZUnloading(ZGC),并确保对应 GC 器已启用 - 凡手动切换 TCCL 的地方,必须严格用 try-finally 恢复原始值:
Thread current = Thread.currentThread();
ClassLoader original = current.getContextClassLoader();
try {
current.setContextClassLoader(myClassLoader);
// do work
} finally {
current.setContextClassLoader(original);
} - Netty 用户务必在应用关闭时显式调用:
GlobalEventExecutor.INSTANCE.shutdownGracefully(),避免后台线程持续持有 ClassLoader
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











