一、💡 一句话理解
核心结论
Java GC 主要通过可达性分析判断对象是否还能被使用:从 GC Roots 出发,找不到任何引用路径连接到的对象,就具备被回收的条件。
二、🧭 理论:它是什么
2.1 可达性分析
可以把对象想成房间,把引用想成连接房间的路,GC Roots 是几个固定入口。
- 从入口还能走到的房间:对象仍然存活。
- 从任何入口都走不到的房间:对象不可达,可以被 GC 回收。
这个类比中:
| 类比 | Java 中的真实含义 |
|---|---|
| 入口 | GC Roots,例如线程栈中的局部变量引用 |
| 房间 | 堆中的对象 |
| 道路 | 对象之间的引用关系 |
最常见的情况确实可以理解为“从线程栈走到堆”:
User user = new User();
user.setAddress(new Address());对应的引用链是:
线程栈中的 user 引用(GC Root)
↓
堆中的 User 对象
↓
堆中的 Address 对象GC 可以沿着 user 找到 User 和 Address,所以它们暂时不能被回收。不过,入口不只有线程栈,还包括静态字段引用和 JVM 内部引用等 GC Roots。
类比只用于帮助理解;真实判断依据是 JVM 中对象之间的引用关系。
三、⚙️ 理论:它是怎么工作的
3.1 GC Roots 是什么
GC Roots 是 JVM 进行可达性分析时使用的一组起点。它不是某个具体的内存区域,也不是一种特殊的对象类型。
JVM 会先把当前肯定还需要使用的一些引用当作“根”,然后沿着引用关系查找堆中的对象:
GC Roots
↓
堆中的对象 A
↓
堆中的对象 B只要对象能从任意 GC Root 到达,它就不能被回收;从所有 GC Roots 都无法到达,才具备被回收的条件。
一句话记忆
GC Roots 就是垃圾回收查找存活对象时的几个可信起点。
3.2 GC Roots 有哪些
面试中记住这些常见来源即可:
3.2.1 活动线程栈中的引用
方法参数和局部变量都可能保存对象引用:
public void process() {
// user 是线程栈中的局部变量,指向堆中的 User 对象。
User user = new User();
user.printName();
}引用关系:
活动线程的栈帧 → 局部变量 user → 堆中的 User 对象只要方法仍可能继续使用 user,这个 User 对象就可以从 GC Roots 到达,不能被回收。方法执行结束或 user 不再被使用后,这条引用链才可能消失。
3.2.2 类的静态字段引用
静态字段跟随已经加载的类存在,可以长期引用堆中的对象:
public class UserCache {
// 静态字段一直引用 User 对象。
private static final User ADMIN = new User();
}引用关系:
已加载的 UserCache 类 → 静态字段 ADMIN → 堆中的 User 对象只要这个类及其静态字段仍然有效,ADMIN 指向的对象通常就不能被回收。静态集合长期保存业务对象,也是常见的内存泄漏原因。
3.2.3 JVM 内部持有的引用
JVM 为了维持程序运行,会保留活动线程、类加载器等内部对象。例如:
Thread worker = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
// 持续处理任务
}
});
worker.start();引用关系可以简化为:
JVM 线程管理 → 正在运行的 Thread → 线程关联的任务对象线程还在运行时,即使业务代码不再保留 worker 变量,JVM 仍需调度这个线程,因此线程对象及它继续引用的对象可能无法回收。
3.2.4 JNI 本地代码持有的引用
JNI 全称是 Java Native Interface(Java 本地接口),可以把它理解成 Java 与 C、C++ 等本地代码之间的“翻译桥梁”:
Java 代码 → JNI → C/C++ 本地代码 → 操作系统或本地库JNI 常用于调用操作系统底层能力、复用已有的 C/C++ 库,以及访问硬件驱动、音视频库或加密库。
Java 使用 native 声明由本地代码实现的方法:
public class NativeDemo {
// native:这个方法只有 Java 声明,具体实现位于 C、C++ 本地库中。
public native void process(User user);
static {
// loadLibrary:加载编译后的本地库,例如 Linux 的 .so 或 Windows 的 .dll。
System.loadLibrary("native_demo");
}
}本地代码接收到 User 后,可以通过 JNI 引用继续持有这个 Java 对象:
引用关系可以简化为:
C/C++ 本地代码 → JNI 引用 → 堆中的 User 对象JNI 局部引用通常在一次本地方法调用期间有效;JNI 全局引用可以长期存在,需要由本地代码主动释放。全局引用一直不释放时,相关 Java 对象就可能长期无法被回收。
一句话理解 JNI
JNI 是 Java 调用 C/C++ 等本地代码的桥梁;本地代码持有的 Java 对象引用,也会影响 GC。
快速区分
线程栈引用表示“Java 方法还可能使用”;静态字段引用表示“类还保存着”;JVM 内部引用表示“JVM 自己还要使用”;JNI 引用表示“C/C++ 本地代码还要使用”。
GC Roots → 对象 A → 对象 B
对象 C ↔ 对象 DA、B 可以从 GC Roots 到达,不能回收。C、D 即使互相引用,只要与 GC Roots 断开,仍然可以被回收。
注意
“不可达”表示对象具备回收条件,不代表 GC 会立即执行,也不能依靠
System.gc()保证某个对象立刻被回收。
3.3 引用强度也会影响回收
引用越弱,GC 越容易回收它指向的对象:
| 引用类型 | 大白话 | 对象什么时候可能被回收 |
|---|---|---|
| 强引用 | 紧紧抓住对象 | 强引用仍然可达时不回收 |
| 软引用 | 内存够就先留着 | 内存紧张时可能回收 |
| 弱引用 | 只贴了一张临时标签 | GC 发现对象仅剩弱引用时就可能回收 |
| 虚引用 | 不取对象,只接收通知 | 对象进入可回收阶段时配合引用队列使用 |
3.3.1 强引用
平时直接创建对象,得到的就是强引用:
// user 是指向 User 对象的强引用。
User user = new User();
// 断开这条强引用后,如果对象也不能从其他 GC Roots 到达,
// 它才具备被回收的条件。
user = null;只要对象仍能通过强引用从 GC Roots 到达,GC 就不会回收它。
3.3.2 软引用 SoftReference
软引用表达的是“内存充足时可以保留,内存紧张时允许回收”:
import java.lang.ref.SoftReference;
User user = new User();
// SoftReference:使用软引用保存 User 对象。
SoftReference<User> softReference = new SoftReference<>(user);
// 去掉原来的强引用,让对象只剩软引用。
user = null;
// get:对象尚未被回收时返回 User,已被回收时返回 null。
User result = softReference.get();软引用曾常用于内存敏感缓存,但回收时间无法精确控制,不能把它当成具有稳定容量和淘汰规则的缓存。
3.3.3 弱引用 WeakReference
弱引用比软引用更弱。对象只剩弱引用时,GC 判断它为弱可达后,就可能清除弱引用并回收对象:
import java.lang.ref.WeakReference;
User user = new User();
// WeakReference:弱引用不会阻止 User 被 GC 回收。
WeakReference<User> weakReference = new WeakReference<>(user);
// 去掉原来的强引用。
user = null;
// 对象被回收后,get() 返回 null。
User result = weakReference.get();ThreadLocalMap 的 key 使用弱引用,但 value 仍是强引用,所以 ThreadLocal 用完后仍要调用 remove()。可结合 10.ThreadLocal的原理和使用场景 理解。
3.3.4 虚引用 PhantomReference
虚引用不是用来取得对象的,调用 get() 始终返回 null。它通常和 ReferenceQueue 配合,用于获知对象已经进入可回收阶段:
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
ReferenceQueue<User> queue = new ReferenceQueue<>();
User user = new User();
// PhantomReference 必须和引用队列一起创建。
PhantomReference<User> phantomReference =
new PhantomReference<>(user, queue);
// 去掉原来的强引用。
user = null;
// 虚引用不能用于取得目标对象,结果始终是 null。
System.out.println(phantomReference.get());当 GC 处理目标对象时,虚引用会按规则进入关联的引用队列,程序可以检查队列并执行后续清理或监控逻辑。
记忆口诀
强引用不回收,软引用内存紧张时收,弱引用 GC 时可能收,虚引用只负责回收通知。
这些引用只能影响对象是否允许被回收,不能保证 GC 在某个确定时间执行。
四、🚀 实践:观察对象变为不可达
4.1 前置准备
准备一个可用的 JDK,无需额外依赖。
4.2 可以拿来干什么
理解可达性分析,可以帮助排查内存泄漏:如果业务对象一直无法回收,通常说明它仍被静态集合、线程、缓存或监听器等对象间接引用。
4.3 最小验证
新建 GcReachabilityDemo.java:
import java.lang.ref.WeakReference;
public class GcReachabilityDemo {
public static void main(String[] args) throws InterruptedException {
Object target = new Object();
// WeakReference 不会像强引用一样阻止目标对象被回收。
WeakReference<Object> weakReference = new WeakReference<>(target);
// 断开局部变量到目标对象的强引用路径。
target = null;
// System.gc:只是向 JVM 提出执行 GC 的建议,不保证立即执行。
System.gc();
Thread.sleep(100);
// 如果输出 null,说明目标对象已经被回收;
// 如果不是 null,也不代表它永远不会回收,只是本次尚未回收。
System.out.println(weakReference.get());
}
}运行:
javac GcReachabilityDemo.java
java GcReachabilityDemo常见输出是 null,但具体回收时机由 JVM 决定,因此结果不保证每次相同。
五、🎤 面试时怎么流利地说
5.1 回答路线
可达性分析 → GC Roots → 引用链 → 不可达才可回收 → 回收时机不确定。
5.2 30 秒标准回答
Java GC 主要通过可达性分析判断对象是否可以回收。它会从 GC Roots 出发,沿着对象引用关系向下查找;能够到达的对象仍然存活,找不到引用路径的对象就具备被回收的条件。常见 GC Roots 包括线程栈中的引用、静态字段引用和 JVM 内部引用。即使两个对象循环引用,只要它们整体与 GC Roots 断开,也可以被回收。需要注意,可以回收不等于会被立即回收。
5.3 高频追问与短答
问:为什么循环引用不会造成 Java GC 无法回收?
因为 Java 不是只看对象的引用计数,而是检查对象能否从 GC Roots 到达。循环中的对象如果与 GC Roots 断开,整个环仍然是不可达的。
六、📌 总结
- 核心算法:可达性分析。
- 判断起点:GC Roots。
- 没有从 GC Roots 到对象的引用路径,对象才具备回收条件。
- 循环引用不等于不能回收。
- 口诀:从根出发,有路则活,无路可收。