满足什么条件时,一个 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+ 小伙伴加入学习,欢迎点击围观

面试考察点

  1. 类生命周期理解:面试官不仅仅是想知道卸载条件,更是想看你对 "加载 → 验证 → 准备 → 解析 → 初始化 → 使用 → 卸载" 这条完整链路有没有整体认知。

  2. 引用关系分析能力:类卸载的核心是引用链的断开。能不能把 Class 对象、类加载器、实例对象三者的引用关系说清楚,直接体现你的 JVM 功底。

  3. 实践认知:很多开发者以为类卸载是常态,其实在大多数应用中类几乎不会被卸载。面试官想看你是否理解这个现实,以及在什么场景下才需要真正关心类卸载。

核心答案

一个类被卸载,必须 同时满足以下 3 个条件

  1. 该类所有的 实例对象 都已被回收(堆中不存在该类的任何实例)
  2. 加载该类的 ClassLoader 已经被回收
  3. 该类对应的 java.lang.Class 对象 没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

满足这三个条件后,JVM 允许 在下次 Full GC 时卸载该类。注意是 "允许",不是 "一定"——最终是否卸载取决于 JVM 的实现。

深度解析

一、为什么要同时满足三个条件?

这得从 JVM 的引用关系说起。类在 JVM 中的存活,靠的是一张引用网:

上图画出了类在 JVM 中的三条 "生命线",任意一条还连着,这个类就卸不掉:

  • ClassLoaderClass 对象:类加载器持有着它所加载的所有 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 会:

  1. 创建一个新的 WebAppClassLoader 实例
  2. 用新的类加载器加载新版本的类
  3. 丢弃对旧 WebAppClassLoader 的所有引用
  4. 旧类加载器、它加载的 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 对象来做反射优化,结果导致类永远卸不掉,元空间持续膨胀。

面试高频追问

  1. Bootstrap ClassLoader 加载的类会被卸载吗?

    不会。Bootstrap ClassLoader 是 JVM 内置的,生命周期与 JVM 相同,永远不会被回收,所以条件二永远不满足。

  2. 元空间和永久代在类卸载上有什么区别?

    卸载条件完全一样。区别在于存储位置:永久代在堆中,大小固定(-XX:MaxPermSize);元空间在本地内存中,默认不限大小。卸载机制本身没有变化,但元空间的引入让类卸载不够及时时的后果更严重了——不再是 java.lang.OutOfMemoryError: PermGen space,而是本地内存被吃光。

  3. 如何监控类卸载情况?

    加上 -XX:+TraceClassUnloading 参数,可以在日志中看到类卸载记录。或者用 JConsole、VisualVM 连上去看 "已卸载类数量" 这个指标。

常见面试变体

  • "JVM 什么时候会卸载一个类?"
  • "方法区会被垃圾回收吗?回收的条件是什么?"
  • "为什么说 Java 的类卸载是很难发生的?"
  • "Tomcat 热部署的原理是什么?和类卸载有什么关系?"

记忆口诀

三个条件:无实例、无加载器、无 Class 引用——三无才能卸。

实际场景:普通应用基本不卸,热部署才需要管。

总结

类卸载要同时满足三个条件——实例全回收、类加载器回收、Class 对象无引用。但在实际应用中,因为 AppClassLoader 不会被回收,你的业务类几乎永远不会被卸载。真正需要关心类卸载的是 Tomcat 热部署、Groovy 动态编译这类场景,核心思路就是用自定义类加载器来隔离类的生命周期。