类加载器内存泄漏的本质是自定义类加载器实例被强引用持有,导致其加载的类元数据滞留metaspace无法回收;排查需聚焦引用链定位,修复须切断静态缓存、threadlocal及tccl三类强引用。

类加载器内存泄漏的核心不是“类没卸载”,而是自定义类加载器实例被强引用钉住,导致它加载的所有类元数据长期滞留在 Metaspace 中无法回收。排查必须聚焦于“谁在持有这个加载器”,而非仅调大 Metaspace 或重启应用。
定位存活的自定义类加载器实例
先确认问题是否存在,再抓取证据:
- 用 jstat -gc
观察 MU(Metaspace Used)持续上涨且 Full GC 后不回落,同时 MC(Metaspace Capacity)缓慢扩大,是典型信号 - 触发堆转储:jmap -dump:format=b,file=heap.hprof
,推荐在疑似泄漏后或 Metaspace OOM 前手动执行 - 用 Eclipse MAT 打开,打开 Class Loader Explorer,筛选名称含 CustomClassLoader、MyClassLoader、URLClassLoader(非 AppClassLoader) 的实例
- 重点关注多个实例的 Retained Heap 大小 和 Loaded Classes 数量——若单个达几 MB 以上且长期不释放,基本可锁定为泄漏源
挖出强引用链:从 GC Roots 入手
在 MAT 中右键可疑 ClassLoader 实例 → Path to GC Roots → exclude weak/soft references,重点检查以下三类路径:
-
静态字段引用:如
public static Map<string object> CACHE</string>中存了该加载器加载的 Service 实例,而该实例内部隐式持有this.getClass().getClassLoader() -
ThreadLocal 持有:Web 容器线程池复用时,Filter 或拦截器将 ClassLoader 或其加载的对象放入 ThreadLocal,但未在 finally 中调用
remove() -
线程上下文类加载器(TCCL)未重置:守护线程(如定时上报、心跳检测)创建时继承了 WebAppClassLoader,又未在
run()开头执行Thread.currentThread().setContextClassLoader(null)或恢复原始加载器
代码级修复关键动作
参数调优只是兜底,真正止血靠切断引用链:
- TCCL 使用必配 try-finally:临时切换前保存旧值,任务结束立即 restore,避免污染线程生命周期
-
静态缓存改用 WeakHashMap 或在应用销毁阶段(如
ServletContextListener.contextDestroyed())主动CACHE.clear() -
ThreadLocal 必须 remove:尤其在 Filter、Interceptor、AOP 切面中,确保每个
set()都有对应remove(),放在 finally 块里 -
自定义类加载器实现 close() 或 destroy():显式清理内部资源(如 JarFile、URLStreamHandlerFactory)、清空缓存字段,并在 Spring 的
DisposableBean.destroy()或 Servlet 生命周期中调用
预防与监控建议
上线前和运行期都要设防:
- JVM 启动加 -XX:MaxMetaspaceSize=256m,避免无限制增长拖垮系统
- 加 -XX:+TraceClassLoading -XX:+TraceClassUnloading,观察热部署后是否出现大量 Loaded 但几乎无 Unloaded 日志
- 对动态代理、脚本引擎等高频生成类场景,禁用
Enhancer.setUseCache(false),预编译并复用 CompiledScript - 使用 JNI 时,确认所有
NewGlobalRef都有配对的DeleteGlobalRef,native 层也能钉住 ClassLoader
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











