
什么样的类会被卸载?
类卸载不是“用完就卸”,而是必须同时满足三个硬性条件:ClassLoader 实例被回收、该类所有 Class 对象无任何强引用、该类加载的字节码和静态变量等元数据不再被任何活跃线程访问。最常见误判是以为“类没再 new 了就能卸”,其实只要它的 Class 对象还被缓存(比如 Spring 的 ConcurrentHashMap 引用)、或某个静态字段还持有着它,就不会卸。
典型场景包括:热部署时自定义 ClassLoader 被 GC;OSGi 插件停用;Web 应用重启(Tomcat 的 WebAppClassLoader 被丢弃)。
容易踩的坑:
- 静态内部类或匿名类会隐式持有外部类的
Class引用,导致外部类无法卸载 -
ThreadLocal里存了该类的实例或Class对象,且没调用remove() - JNI 全局引用(
jobject)未释放,JVM 会认为类仍在使用中
元空间(Metaspace)什么时候真正回收?
元空间回收不等于类卸载,它只在类卸载后才可能释放对应内存。但即使类卸了,元空间也不会立刻归还给操作系统——它先留在 JVM 内部的空闲块池里,供后续新类加载复用。只有触发完整 GC(如 System.gc() 或 CMS/Full GC)且元空间碎片整理完成,才会把大块连续内存交还 OS。
立即学习“Java免费学习笔记(深入)”;
关键参数影响:
-
-XX:MaxMetaspaceSize设得太小,会频繁触发 GC,反而增加开销 -
-XX:MinMetaspaceFreeRatio和-XX:MaxMetaspaceFreeRatio控制是否收缩元空间,默认值(40/70)下,空闲率超 70% 才考虑收缩 - JDK 8u40+ 后,元空间默认启用压缩指针(
-XX:+UseCompressedClassPointers),对 64 位 JVM 节省内存明显
怎么验证类是否真的卸载了?
不能只看 jstat -gc 的 MU(Metaspace used)下降,那只是元空间已用内存减少,未必是类卸载——也可能是类加载失败后回滚。真正证据得从 GC 日志或 JFR 中找:
开启 -XX:+TraceClassUnloading,看到类似这样的输出才算实锤:
[Unloading class demo.MyService 0x00000008002a1000]
或者用 jcmd <pid> VM.native_memory summary scale=MB</pid> 对比前后 “class” 区域变化,再结合 jmap -histo:live 确认 MyService 类实例数归零。
注意:jmap -clstats 显示的是类加载器统计,不是卸载日志;jconsole 的 MBean 中 java.lang:type=MemoryPool,name=Metaspace 的 Usage.used 下降也不代表类卸了,只是元空间用了更少内存。
为什么线上很少见到类卸载成功?
因为绝大多数应用框架(Spring、Log4j、Jackson)都在启动期把类加载进系统类加载器或共享类加载器,而这些加载器生命周期与 JVM 一致,永远不会被 GC。你写的业务类哪怕只用一次,只要它的 Class 对象被 Spring 的 GenericApplicationContext 缓存过,就锁死了卸载路径。
真正能触发类卸载的,基本只出现在以下情况:
- 用
URLClassLoader动态加载 JAR,并显式置 null + 确保无其他引用 - 使用模块化(Java 9+ ModuleLayer)并主动
controller.close() - Quarkus / GraalVM 原生镜像里根本没类卸载概念——类在编译期就固化了
所以别指望靠“让类卸载”来解决长期运行服务的内存增长问题。元空间涨得慢,但一旦涨上去,往往意味着有类加载器泄漏,而不是类本身该卸没卸。










