jmap -histo:live 21455 | grep -i classloader
jcmd 21455 GC.class_histogram | head -20
jmap -dump:live,format=b,file=/tmp/heap.hprof 21455

测试环境每发一版就热部署一次,二十次后 Metaspace 涨到 780MB,jmap 直方图里 WebappClassLoader 有 3800 个实例,老版本一直没被回收。先别急着动参数,把报错原文和时间点记下来,顺序反了容易白折腾。

原因不复杂,应用里有静态字段持有线程、JDBC 驱动或 ThreadLocal,线程的 contextClassLoader 指回旧加载器。只要还有一个强引用,整个 ClassLoader 和它加载的类都留着。

「ClassLoader 泄漏」排查到最后往往是很小的一处差异,前面那些复杂猜测可以先放一放。动手前把原文件复制一份带时间戳,出问题立刻换回去,比现场分析快得多。

一次只改一处,改完立刻验证,几个改动叠在一起,出问题就说不清是谁导致的。先做最小复现,能稳定跑出来的问题,排查成本会低一个数量级。

处理上按这个顺序:用 jmap -histo 和 MAT 查旧 ClassLoader 的 GC Root 路径;在 ServletContextListener 里注销驱动和定时器 关闭线程池;ThreadLocal 用完 remove 别留在容器线程上;验证时连续部署五次看实例数是否回到 1。

改完只确认一件事:连续热部署五次后 WebappClassLoader 实例数保持为 1 Metaspace 不再阶梯上涨。

作者 9823189