类卸载失败即为元空间泄漏:若压测后mu持续上涨、mgcc为0且卸载日志极少,说明类加载器未回收;需通过jmap-clstats、jmap-histo与mat分析类加载器存活、动态类生成路径及强引用残留。

核心判断逻辑很直接:类卸载成功,元空间使用量就该回落;压测结束后持续上涨或不降,基本就是泄漏了。关键不在“有没有类生成”,而在于“生成的类有没有被真正清理掉”。
确认类卸载是否实际发生
启动JVM时加上这两个参数,压测前后对比日志:
-
-XX:+TraceClassUnloading:只输出类卸载记录,每行形如
[Unloading class com.example.DynamicService$$EnhancerByCGLIB$$a1b2c3d4 (0x0000000800abc000)] - -verbose:class:同时打印加载和卸载(信息量大,生产慎用,压测环境可用)
压测结束后观察日志中卸载行数量。如果加载了上万次动态类,但卸载记录只有个位数,说明卸载机制根本没生效。
结合jstat验证元空间回收行为
在压测中和结束后持续运行:
jstat -gc
- 重点关注 MU(Metaspace Used)和 MC(Metaspace Capacity)是否持续单边上涨
- 看 MGCC(Metaspace GC 次数)是否为 0 或长期不变——说明没有触发元空间回收,类卸载条件未满足
- 若 MU 接近 MC 且 MGCC 仍为 0,基本可断定类加载器未被回收,类无法卸载
定位泄漏源头的三步交叉法
不是查“哪个类多了”,而是查“谁让这些类一直活著”:
-
查类加载器存活状态:执行 jmap -clstats
,看是否有大量 sun.reflect.DelegatingClassLoader、org.springframework.boot.loader.LaunchedURLClassLoader或自定义类加载器实例,且数量随压测轮次递增 -
查类名特征与生成路径:用 jmap -histo:live
| grep -E "(Enhancer|Proxy|Accessor|$$)" 筛出高频动态类,再结合 jstack搜索对应类名出现的调用栈(例如看到大量 CglibAopProxy.getProxy或Method.invoke) -
查强引用残留:用 MAT 打开堆 dump,按
java.lang.Class聚类,对可疑类右键 → “Merge Shortest Paths to GC Roots”,排除exclude weak/soft references,看是否被反射对象(Method、Field)、静态缓存、日志框架或 AOP 增强器强持有
压测后快速验证卸载条件是否满足
写一段诊断代码,在压测结束、所有业务线程停稳后手动触发:
- 调用
System.gc()和System.runFinalization()(仅辅助,不保证) - 检查关键类加载器是否还可达:
ClassLoader cl = YourDynamicClass.class.getClassLoader(); System.out.println(cl == null || cl.getParent() == null ? "可能已不可达" : "仍活跃"); - 检查该类是否有实例残留:
long count = Arrays.stream(ManagementFactory.getMemoryPoolMXBeans()) .filter(p -> p.isUsageThresholdSupported()) .count(); // 实际应配合 jmap -histo 或 JOL 工具统计实例数
三个条件任一不满足,类就卡在元空间里出不去——这才是泄漏的本质。










