类加载器内存泄漏本质是ClassLoader实例被强引用钉住导致Metaspace无法卸载其加载的类,需重点排查静态集合、未清理ThreadLocal、守护线程ContextClassLoader三类锚点,并在应用销毁时主动清理、限制Metaspace大小、慎用动态类生成。

类加载器内存泄漏不是“类没卸载”,而是它自己被钉住了——只要 ClassLoader 实例还活着,它加载的所有类元数据就一直卡在 Metaspace 里,哪怕应用早已下线。
盯住三类强引用锚点
ClassLoader 被意外持有时,通常藏在以下位置:
-
静态集合或缓存:比如 static Map
存了某个 WebApp 加载的 Service 实例,这个 map 一存在,整个 ClassLoader 就动不了 - ThreadLocal 未清理:线程池复用场景下,若业务逻辑往 ThreadLocal 放了来自当前上下文类加载器的对象,又没在 finally 或 afterCompletion 中 remove(),线程下次执行时仍带着旧 ClassLoader 的引用
- 守护线程 + ContextClassLoader:自启动线程(如定时上报线程)创建时会继承父线程的 context class loader;若未在 run() 开头显式重置为 Thread.currentThread().setContextClassLoader(null),该线程生命周期就等于 ClassLoader 生命周期
卸载前主动清理上下文
类卸载不靠等,得靠清。关键动作要写在应用销毁阶段:
- 在 ServletContextListener.contextDestroyed() 中调用 clearReferences() 类似逻辑
- Spring 环境下,在实现 DisposableBean 的 destroy() 方法里遍历并清理静态缓存、注销监听器、remove ThreadLocal
- 若用了 JNI,确认所有 NewGlobalRef 都配对了 DeleteGlobalRef,否则 native 层也会钉住 ClassLoader
限制 Metaspace + 监控回收行为
不限制上限的 Metaspace 会让泄漏更隐蔽也更危险:
立即学习“Java免费学习笔记(深入)”;
- 加 JVM 参数:
-XX:MaxMetaspaceSize=256m显式设上限,避免拖垮系统内存 - 搭配
-XX:MetaspaceSize=128m提前触发 GC 和类卸载尝试 - 启用
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps,关注日志中 “Full GC (Metadata GC Threshold)” 是否出现,以及 Metaspace 使用量是否回落 - 运行时用
jstat -gc <pid>查看 MC(Metaspace Capacity)、MU(Metaspace Used)变化趋势,持续上涨且不降就是典型信号
慎用动态类生成机制
CGLIB、Javassist、Groovy、Kryo 动态注册类等操作,每生成一个新类,就在 Metaspace 多占一块地:
- 代理类优先复用已有实例,避免每次请求都 new 一个新 Enhancer
- 脚本引擎尽量预编译、缓存 CompiledScript,而非每次 eval 新脚本
- 序列化框架开启类注册白名单,禁用运行时自动注册未知类型


















