元空间垃圾回收的核心是类卸载,需同时满足三条件:所有实例被回收、classloader被回收、class对象无强引用;仅在full gc或特定并发gc阶段被动触发,不依赖内存使用量阈值。

方法区的垃圾回收,核心在于“类卸载”,而不是单纯看内存用了多少。元空间(Metaspace)不会因为占用变高就自动清理,它只在满足卸载条件、且触发对应 GC 时,才真正释放内存。
类卸载必须同时满足的三个硬性条件
少一个,类就卡在元空间里不走:
- 该类所有 Java 堆实例已被回收——包括子类对象、内部类隐式持有的外部类实例,以及静态集合中残留的对象引用
- 加载它的 ClassLoader 实例已被 GC 回收——系统类加载器(如 AppClassLoader)基本不会被回收;能被回收的,主要是 WebAppClassLoader、URLClassLoader 等自定义加载器
- 该类对应的 Class 对象无任何强引用——常见泄漏点有:static 缓存、ThreadLocal 未 remove、反射 setAccessible 后未清理、JNI 全局引用未配对释放
元空间回收不是独立发生的,而是依赖 GC 类型和时机
它不自己决定“该清了”,而是被动配合:
Java JDK 25 来自 OpenJDK 官方归档,版本为 JDK 25,本条下载地址已指向官方 Windows x64 zip 安装包直链,适合调试旧项目或兼容旧版 Java 运行环境。
- 仅在 Full GC 或某些并发 GC 阶段(如 G1 的并发标记周期末尾)才执行类卸载逻辑
- 元空间使用量达到 -XX:MetaspaceSize 初始阈值,会触发一次 GC,尝试卸载;超 -XX:MaxMetaspaceSize 则强制触发 Full GC
- System.gc() 只是建议,JVM 可忽略;它不保证触发元空间回收,更不保证类卸载成功
为什么线上很少看到元空间明显下降?
不是没回收,而是条件难凑齐:
- Spring、MyBatis 等框架大量缓存 Class 对象,且常通过 static 字段或 ApplicationContext 持有
- 线程未清理上下文类加载器(Thread.currentThread().setContextClassLoader(null) 被忽略)
- 热部署失败后旧 ClassLoader 残留,但新请求已用新 loader,旧 loader 却仍被线程池、监听器等间接引用
- 即使类卸载成功,元空间内存也不会立刻还给操作系统,而是先进入 JVM 内部空闲池复用
怎么确认类真的被卸载了?
别只盯着 jstat 的 MU 值下降,那可能是加载失败回滚,不是回收:
- 加参数 -XX:+TraceClassUnloading,日志里出现 [Unloading class xxx] 才算实锤
- 用 jcmd
VM.native_memory summary scale=MB 对比前后 “class” 区域变化 - 配合 jmap -histo:live 查看目标类实例数是否归零,再结合 ClassLoader 统计交叉验证










