要搞懂「Class 会不会回收」和「怎么回收」,咱们先抛开枯燥的术语,从一个 「图书馆管理员与临时图书」 的故事讲起,再结合代码和时序图拆解原理,保证小白也能秒懂!
一、先讲故事:Class 回收的本质是「没人要的临时资产」
把 JVM 想象成一个 「手机系统图书馆」,里面有这些角色:
-
JVM GC:图书馆清洁工,专门清理没人用的 "垃圾";
-
类加载器:图书馆管理员,负责 "引进图书"(加载 Class);
- 「系统管理员」(Bootstrap/Extension/Application ClassLoader):官方固定员工,管的是《系统操作手册》《基础工具集》这类 "常备书"(如
java.lang.String
、android.os.Bundle
),这些书天天有人用,清洁工绝不会收; - 「临时管理员」(自定义 ClassLoader):临时雇来的,负责引进一些 "小众临时书"(如插件类、热修复类),这些书用完没人要了,就会被清洁工收走;
- 「系统管理员」(Bootstrap/Extension/Application ClassLoader):官方固定员工,管的是《系统操作手册》《基础工具集》这类 "常备书"(如
-
Class 对象:图书的 "实体副本",管理员引进书后,会生成这个副本供人使用;
-
普通对象:读者借走的 "书的复印件",用完还回去(引用置 null),很快会被回收,但 "实体副本"(Class)是否回收,要看管理员和副本本身有没有人要。
核心结论(故事版):
-
Class 能回收吗? 能!但只限于「临时管理员引进的临时书」(自定义 ClassLoader 加载的 Class)。「系统管理员的常备书」(系统类)永远不会被回收,因为管理员和书一直有人用。
-
怎么让 Class 被回收? 必须满足两个 "没人要":
- ① 「临时管理员」没人要了(自定义 ClassLoader 实例无任何引用);
- ② 「图书实体副本」没人要了(Class 对象无任何引用:没有实例、没有静态变量持有、没有反射引用等)。
二、代码实战:亲眼见证 Class 回收
咱们用代码模拟 "临时管理员引进临时书,用完回收" 的过程,关键是用 弱引用(WeakReference) 观察 Class 是否被回收(弱引用不会阻止 GC 回收,对象被回收后,弱引用会返回 null)。
步骤 1:准备 "临时书"(TestClass)
这是一本 "临时书",没有静态变量(避免持有 Class 引用),只有一个简单方法:
java
// 临时书:TestClass(没有静态变量,避免干扰回收)
public class TestClass {
public void sayHello() {
System.out.println("临时书:Hello,我是被临时管理员加载的!");
}
}
步骤 2:准备 "临时管理员"(自定义 ClassLoader)
自定义一个 ClassLoader,负责加载 "临时书"。注意:必须重写 findClass
而非 loadClass
,因为 loadClass
默认遵循「双亲委派」(先让系统管理员加载),我们要强制用 "临时管理员" 加载,所以只在系统管理员找不到时,自己加载:
java
// 临时管理员:CustomClassLoader(自定义类加载器)
public class CustomClassLoader extends ClassLoader {
// 从字节数组加载 Class(模拟"引进临时书")
public Class<?> loadCustomClass(String className, byte[] classBytes) {
// 1. 先检查是否已加载过(避免重复加载)
Class<?> loadedClass = findLoadedClass(className);
if (loadedClass != null) {
return loadedClass;
}
// 2. 双亲委派:让父加载器(系统管理员)先找,找不到再自己加载
try {
loadedClass = getParent().loadClass(className);
} catch (ClassNotFoundException e) {
// 父加载器找不到,自己加载(核心:临时管理员的工作)
loadedClass = defineClass(className, classBytes, 0, classBytes.length);
}
return loadedClass;
}
}
步骤 3:模拟 "借书、用书、还书、回收"
核心逻辑:用临时管理员加载书 → 借书用 → 用完后把 "书的复印件""实体副本""管理员" 都置为 null → 叫清洁工(GC)来清理 → 观察 Class 是否被回收:
java
import java.lang.ref.WeakReference;
public class ClassRecyclerDemo {
public static void main(String[] args) throws Exception {
// 1. 准备"临时书"的字节码(这里简化:从已编译的 TestClass.class 读取)
byte[] testClassBytes = getClassBytes("TestClass.class");
// 2. 雇"临时管理员"(创建 CustomClassLoader 实例)
CustomClassLoader tempLoader = new CustomClassLoader();
// 3. 管理员引进"临时书"(加载 TestClass,生成 Class 对象)
Class<?> testClass = tempLoader.loadCustomClass("TestClass", testClassBytes);
// 4. 借"书的复印件"(创建 TestClass 实例,用一下)
Object testInstance = testClass.getConstructor().newInstance();
testClass.getMethod("sayHello").invoke(testInstance); // 输出:临时书:Hello...
// 5. 用完了!把"复印件""实体书""管理员"都置为 null(关键:切断所有引用)
testInstance = null;
WeakReference<Class<?>> testClassWeakRef = new WeakReference<>(testClass); // 弱引用观察回收
testClass = null; // 切断 Class 对象的强引用
tempLoader = null; // 切断临时管理员的强引用
// 6. 叫清洁工(GC)来巡逻(注意:System.gc() 是"建议",不是强制,但演示足够)
System.out.println("=== 叫清洁工(GC)来清理 ===");
System.gc();
Thread.sleep(1000); // 等 GC 执行完
// 7. 观察:"临时书"(TestClass)是否被回收
if (testClassWeakRef.get() == null) {
System.out.println("✅ 临时书(TestClass)被回收了!");
} else {
System.out.println("❌ 临时书(TestClass)没被回收...");
}
}
// 辅助方法:读取 .class 文件的字节数组(模拟加载类文件)
private static byte[] getClassBytes(String className) throws Exception {
try (InputStream is = ClassRecyclerDemo.class.getClassLoader().getResourceAsStream(className);
ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
byte[] buf = new byte[1024];
int len;
while ((len = is.read(buf)) != -1) {
baos.write(buf, 0, len);
}
return baos.toByteArray();
}
}
}
代码运行结果(关键):
text
临时书:Hello,我是被临时管理员加载的!
=== 叫清洁工(GC)来清理 ===
✅ 临时书(TestClass)被回收了!
如果我们加个 静态变量 干扰(比如在 TestClass 里加 private static Object obj = new Object();
),结果会变成 ❌ 没被回收
------ 因为静态变量属于 Class 对象,会一直持有 Class 的引用,GC 无法回收!
三、原理拆解:Class 回收的 3 个核心条件
通过故事和代码,我们能提炼出 Class 被回收的 3 个严格条件(缺一不可):
- Class 对象无任何强引用 :没有地方再用这个 Class 了(比如
testClass = null
,没有反射持有它,没有Class.forName
残留引用); - 加载它的 ClassLoader 无任何强引用 :「临时管理员」没人要了(
tempLoader = null
),因为 Class 对象内部会持有对它的加载器的引用(testClass.getClassLoader()
能拿到),如果加载器还在,Class 肯定不会被回收; - 该 Class 的所有实例都已被回收 :没有 "书的复印件" 在外面(
testInstance = null
),实例会持有对 Class 的引用(instance.getClass()
),实例没回收,Class 也没法回收。
为什么系统类(如 String)永远不回收?
因为加载它们的「系统管理员」(如 Application ClassLoader)会被 JVM 一直持有引用(JVM 运行期间需要用这些管理员加载系统类),所以系统管理员永远不回收,它加载的 Class 也永远不回收。
四、时序图:Class 回收的完整流程
用 Mermaid 时序图直观展示整个过程(从加载到回收):

JVM 清洁工(GC)临时书(TestClass 的 Class 对象)临时管理员(CustomClassLoader)应用程序(你)JVM 清洁工(GC)临时书(TestClass 的 Class 对象)临时管理员(CustomClassLoader)应用程序(你)父加载器找不到,自己加载创建 CCL 实例(雇临时管理员)调用 loadCustomClass("TestClass")(要引进书)双亲委派:父加载器查找(系统管理员找)调用 defineClass()(生成 Class 对象)返回 TestClass 对象(书到手)创建实例、调用方法(用书)实例置 null(还复印件)Class 置 null(断实体书引用)CCL 置 null(让管理员下班)调用 System.gc()(叫清洁工)检测 CCL 无引用(标记管理员)检测 TestClass 无引用 + 加载器已标记(标记书)回收 CCL(清理管理员)回收 TestClass(清理书)检查 WeakReference(看书没了)输出"✅ 临时书被回收"
五、总结:必记的 3 句话
- Class 能回收,但只限于 "临时的" :自定义 ClassLoader 加载的 Class 能回收,系统类(如 String)永远不回收;
- 回收的关键是 "没人要" :Class 对象、加载它的 ClassLoader、所有实例,必须都没有强引用;
- 别踩坑:静态变量、反射引用会 "粘住" Class,导致无法回收,想回收就先清掉这些引用。
下次有人问你 "Class 会不会回收",你就把这个图书馆的故事讲给他听,再甩上代码和时序图,保证他秒懂!