满足什么条件时,一个 Java 类会被卸载?
一则或许对你有用的小广告
欢迎加入小哈的星球,你将获得:专属的实战项目(4个项目都能学) / 1v1 提问 / 简历修改 / Java 学习路线 / 社群讨论 / 学习打卡 / 每月赠书
《Spring AI 项目实战(问答机器人、RAG 智能客服、联网搜索)》已完结,基于
Spring AI + Spring Boot 3.x + JDK 21...,查看介绍《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot 3.x + JDK 17...,查看介绍;演示链接:http://116.62.199.48:7070/《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接:http://116.62.199.48/
新开坑项目:《从零手撸:秒杀系统高并发优化实战》 正在更新中...,查看介绍
截止目前,星球内专栏累计输出 150w+ 字,讲解图 5110+ 张,还在持续爆肝中.. 后续还会上新更多项目,已有 4700+ 小伙伴加入学习,欢迎点击围观
面试考察点
-
类生命周期理解:面试官不仅仅是想知道卸载条件,更是想看你对 "加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载" 这条完整链路有没有整体认知。
-
引用关系分析能力:类卸载的核心是引用链的断开。能不能把
Class对象、类加载器、实例对象三者的引用关系说清楚,直接体现你的 JVM 功底。 -
实践认知:很多开发者以为类卸载是常态,其实在大多数应用中类几乎不会被卸载。面试官想看你是否理解这个现实,以及在什么场景下才需要真正关心类卸载。
核心答案
一个类被卸载,必须 同时满足以下 3 个条件:
- 该类所有的 实例对象 都已被回收(堆中不存在该类的任何实例)
- 加载该类的
ClassLoader已经被回收 - 该类对应的
java.lang.Class对象 没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
满足这三个条件后,JVM 允许 在下次 Full GC 时卸载该类。注意是 "允许",不是 "一定"——最终是否卸载取决于 JVM 的实现。
深度解析
一、为什么要同时满足三个条件?
这得从 JVM 的引用关系说起。类在 JVM 中的存活,靠的是一张引用网:
上图画出了类在 JVM 中的三条 "生命线",任意一条还连着,这个类就卸不掉:
-
ClassLoader→Class对象:类加载器持有着它所加载的所有Class对象的引用。只要类加载器还活着,它加载的类就不会被卸载。这也是为什么 Bootstrap ClassLoader 加载的核心类永远不会被卸载——因为 Bootstrap ClassLoader 本身不会被回收。 -
Class对象 → 实例对象:每个实例对象的getClass()方法能追溯到它的Class对象。反过来,Class对象也能通过newInstance()等方式创建实例。不过 GC 判定类是否可卸载时,主要看的是实例对象是否全部被回收。 -
应用代码 →
Class对象:代码中可能直接持有Class对象的引用(比如MyClass.class),或者通过反射(Class.forName("com.xxx.MyClass"))获取并缓存了Class引用。只要这些引用还在,类就卸不掉。
所以你看,这三个条件本质上是同一件事:把指向这个类的所有引用链全部切断。
二、类卸载发生的实际场景
说实话,在绝大多数 Java 应用中,类卸载几乎不会发生。因为你的应用代码是由 AppClassLoader(应用类加载器)加载的,而 AppClassLoader 的生命周期和 JVM 一样长,永远不会被回收。所以条件二就卡死了——你的业务类基本不会被卸载。
真正需要关心类卸载的场景:
| 场景 | 说明 |
|---|---|
| 热部署 / 热加载 | Tomcat、Jetty 等 Web 容器,每次重新部署应用时会创建新的 WebAppClassLoader,旧的类加载器连同它加载的类一起被回收 |
| SPI 机制 | 使用 Thread.getContextClassLoader() 创建新的类加载器加载 SPI 实现,用完后释放 |
| Groovy / JSP 动态编译 | 每次修改脚本会生成新的类加载器来加载编译后的类,旧的需要被卸载 |
| OSGi 模块化 | 每个 Bundle 有自己的类加载器,卸载 Bundle 时对应的类加载器被回收 |
Tomcat 的热部署就是一个典型例子。当你重新部署一个 war 包时,Tomcat 会:
- 创建一个新的
WebAppClassLoader实例 - 用新的类加载器加载新版本的类
- 丢弃对旧
WebAppClassLoader的所有引用 - 旧类加载器、它加载的
Class对象、以及残留的实例对象,在一次 Full GC 中被一起回收
三、方法区 / 元空间的垃圾回收
类卸载本质上是 方法区(JDK 7 及以前)或元空间(JDK 8+)的垃圾回收。
方法区/元空间中存放的是类的元数据信息,包括:
- 类的版本、字段、方法、接口等信息
- 运行时常量池
- 字节码指令
这些元数据也需要被垃圾回收,但 HotSpot 对方法区的 GC 条件非常苛刻——只有满足前面说的三个条件,才会回收。而且方法区的垃圾回收主要发生在 Full GC 阶段,频率远低于堆的 Minor GC。
JDK 8 把永久代(PermGen)改成了元空间(Metaspace),内存分配在本地内存(Native Memory)中,默认没有上限。这意味着如果类卸载不及时,元空间可能无限膨胀,最终被系统 OOM Killer 干掉。所以如果你在用动态类加载的场景,一定要配合 -XX:MaxMetaspaceSize 设置元空间上限。
四、一个容易踩的坑
很多面试者以为只要对象被回收了,类就会被卸载。这是错的。看一个例子:
// 自定义类加载器
public class MyClassLoader extends ClassLoader {
public Class<?> load(byte[] bytecode) {
return defineClass(null, bytecode, 0, bytecode.length);
}
}
MyClassLoader loader = new MyClassLoader();
Class<?> clazz = loader.load(classBytes);
Object instance = clazz.newInstance();
// 第一步:干掉实例
instance = null; // ✅ 条件一满足
// 第二步:Class 对象还在被引用!
Class<?> cached = clazz; // ❌ 条件三不满足
上面的代码中,即使实例对象被回收了,只要 cached 变量还持有 Class 对象的引用,这个类就不会被卸载。在实际开发中,很多人用 ConcurrentHashMap 缓存 Class 对象来做反射优化,结果导致类永远卸不掉,元空间持续膨胀。
面试高频追问
-
Bootstrap ClassLoader 加载的类会被卸载吗?
不会。
Bootstrap ClassLoader是 JVM 内置的,生命周期与 JVM 相同,永远不会被回收,所以条件二永远不满足。 -
元空间和永久代在类卸载上有什么区别?
卸载条件完全一样。区别在于存储位置:永久代在堆中,大小固定(
-XX:MaxPermSize);元空间在本地内存中,默认不限大小。卸载机制本身没有变化,但元空间的引入让类卸载不够及时时的后果更严重了——不再是java.lang.OutOfMemoryError: PermGen space,而是本地内存被吃光。 -
如何监控类卸载情况?
加上
-XX:+TraceClassUnloading参数,可以在日志中看到类卸载记录。或者用 JConsole、VisualVM 连上去看 "已卸载类数量" 这个指标。
常见面试变体
- "JVM 什么时候会卸载一个类?"
- "方法区会被垃圾回收吗?回收的条件是什么?"
- "为什么说 Java 的类卸载是很难发生的?"
- "Tomcat 热部署的原理是什么?和类卸载有什么关系?"
记忆口诀
三个条件:无实例、无加载器、无 Class 引用——三无才能卸。
实际场景:普通应用基本不卸,热部署才需要管。
总结
类卸载要同时满足三个条件——实例全回收、类加载器回收、Class 对象无引用。但在实际应用中,因为 AppClassLoader 不会被回收,你的业务类几乎永远不会被卸载。真正需要关心类卸载的是 Tomcat 热部署、Groovy 动态编译这类场景,核心思路就是用自定义类加载器来隔离类的生命周期。
