一、为什么需要垃圾回收?
在 Java 中,我们通常通过 new 创建对象:
java
User user = new User();
对象创建后,会占用 JVM 堆中的内存空间。
如果对象不再被使用,内存却一直无法释放,那么随着程序运行,对象会越来越多,最终可能导致堆内存不足,出现:
text
java.lang.OutOfMemoryError: Java heap space
在 C/C++ 等语言中,开发者通常需要更直接地管理内存的申请与释放。而 Java 提供了自动垃圾回收机制(Garbage Collection,简称 GC),由 JVM 负责识别和回收不再需要保留的对象。
垃圾回收的核心任务:判断哪些对象仍然存活,并回收已经可以回收的对象所占用的内存。
不过要注意,GC 判断的是对象是否仍然能够从程序运行时的引用关系中到达,而不是判断业务上"以后还会不会用到它"。
二、垃圾回收主要回收哪些区域?
结合前面学习过的 JVM 运行时数据区,可以先区分一下:
| 内存区域 | 是否涉及 GC | 说明 |
|---|---|---|
| Java 堆 | 是,主要区域 | 对象实例通常在堆中分配,是垃圾回收重点 |
| 方法区 / 元空间 | 是 | 可以回收无用的类元数据、常量等,条件比较苛刻 |
| Java 虚拟机栈 | 通常不依赖 GC 回收栈帧 | 方法执行结束后,栈帧随线程执行过程自动出栈 |
| 程序计数器 | 通常不需要 GC | 线程私有,生命周期随线程 |
| 本地方法栈 | 通常不依赖 Java 堆 GC | 由本地方法执行和线程栈生命周期管理 |
本篇主要讨论 Java 堆中的对象如何判断是否可以回收。
三、如何判断一个对象已经成为垃圾?
这是 JVM 垃圾回收最基础、也最重要的问题。
假设我们创建了一个对象:
java
User user = new User();
当变量 user 不再引用这个对象时,它是否就一定成为垃圾了?
不一定。
因为可能还有其他对象引用它。比如:
java
User user1 = new User();
User user2 = user1;
user1 = null;
虽然 user1 已经不再引用对象,但 user2 仍然引用它,所以这个对象仍然存活。
那么 JVM 到底如何判断对象是否可以回收?
常见的思路有两种:
- 引用计数法(Reference Counting)
- 可达性分析算法(Reachability Analysis)
1. 引用计数法
引用计数法的思路比较直观:
为每个对象维护一个引用计数器,每当有一个地方引用它时,计数器加 1;引用失效时,计数器减 1。当计数器为 0 时,就认为对象可以被回收。
例如:
java
User user1 = new User();
User user2 = user1;
此时同一个对象被两个变量引用,可以简单理解为:
text
对象 User
引用计数:2
当执行:
java
user1 = null;
引用计数减为 1。
再执行:
java
user2 = null;
引用计数变为 0,对象就可以被回收。
看起来很简单,但它有一个致命问题:无法正确处理循环引用。
2. 引用计数法的问题:循环引用
例如:
java
class A {
B b;
}
class B {
A a;
}
public class Main {
public static void main(String[] args) {
A a = new A();
B b = new B();
a.b = b;
b.a = a;
a = null;
b = null;
}
}
执行完之后:
- A 对象引用 B 对象。
- B 对象引用 A 对象。
- 外部变量
a和b已经不再引用它们。
从业务角度看,这两个对象已经没有用了。
但是引用计数法中:
- A 的引用计数仍然是 1,因为 B 还引用 A。
- B 的引用计数仍然是 1,因为 A 还引用 B。
两个对象的计数都不为 0,因此仅靠引用计数无法判断它们已经没有外部引用。

这就是引用计数法的主要缺陷。
需要注意:引用计数法并不是所有垃圾回收系统都不用,但它不是 HotSpot JVM 判断 Java 对象是否存活的主要算法。
四、可达性分析算法(Reachability Analysis)
Java 虚拟机主要通过可达性分析算法来判断对象是否存活。
它的核心思想是:
从一组称为 GC Roots 的对象出发,向下搜索对象之间的引用关系。如果某个对象无法通过任何引用链从 GC Roots 到达,那么它就被认为是不可达对象,可以被判定为可回收对象。
先理解三个概念:
- GC Roots:垃圾收集的起点。
- 引用链:对象之间通过引用形成的路径。
- 可达:从 GC Roots 出发,沿着引用关系能够找到这个对象。

例如图中:
- GC Roots 能够通过引用链找到 A。
- A 又引用了 B。
- 所以 A 和 B 都是可达对象。
- C 和 D 虽然互相引用,但没有任何 GC Roots 能够到达它们,因此它们是不可达对象。
这也解释了为什么可达性分析能够解决循环引用问题。
即使两个对象互相引用,只要从 GC Roots 出发无法到达它们,就不会因为这层循环引用而一直被保留。
五、什么是 GC Roots?
GC Roots 可以理解为 JVM 进行可达性分析时的起点。
那么哪些对象可以作为 GC Roots 呢?
常见的来源包括:
1. 虚拟机栈中引用的对象
例如:
java
public void test() {
User user = new User();
// user 局部变量引用的对象
}
当前活动线程的栈帧中,局部变量表所引用的对象,在符合 JVM 根集合规则时可以作为 GC Roots。
2. 类的静态字段引用的对象
例如:
java
public class Cache {
public static User user = new User();
}
被存活类的静态字段引用的对象,可以通过静态字段形成从 GC Roots 出发的引用链。
3. 本地方法中的 JNI 引用
Java 程序可以通过 JNI 调用本地代码。
本地方法中的 JNI 局部引用和全局引用所指向的对象,也可能成为 GC Roots。
4. JVM 内部引用的对象
例如:
- 活动线程。
- 系统类加载器及 JVM 内部维护的相关对象。
- 被同步锁持有等原因需要保持存活的对象。
具体的根集合由 JVM 实现维护,不同 JVM 和不同收集器可能存在实现细节差异。
这里要注意:不要简单地认为常量池里的所有内容都是 GC Roots。是否构成根,要看具体引用关系和 JVM 的根集合规则。
六、对象不可达,就会立即被回收吗?
不一定。
这是一个很容易被忽略的细节。
当对象从 GC Roots 出发已经不可达时,说明它具备了被回收的条件,但不代表 JVM 必须马上回收它。
实际什么时候回收,取决于垃圾收集器的运行时机和回收策略。
另外,Java 还存在一个特殊机制:finalize()。
1. finalize() 方法
如果一个对象重写了 finalize(),并且它此前还没有执行过该方法,那么在对象被判定为不可达时,JVM 可能安排它执行 finalize()。
例如:
java
public class User {
@Override
protected void finalize() throws Throwable {
System.out.println("对象即将被回收");
}
}
在 finalize() 中,对象甚至可以重新建立外部引用,让自己重新变成可达对象,这种情况称为对象复活。
但要注意:
finalize()最多由 JVM 自动调用一次。- 是否及时执行不能保证。
- 如果对象在
finalize()中重新建立了引用,它可能逃过本轮回收。 finalize()已被弃用,并且在较新的 JDK 中已标记为计划移除,不应该在新代码中使用。
因此,实际开发中不要依赖 finalize() 管理资源或控制对象生命周期。
七、Java 的四种引用类型
前面我们提到,对象是否存活,和它是否还存在引用关系密切相关。
但 Java 并不是只有普通引用。
Java 提供了四种引用类型:
- 强引用(Strong Reference)
- 软引用(Soft Reference)
- 弱引用(Weak Reference)
- 虚引用(Phantom Reference)
它们的区别主要在于:GC 面对不同强度的引用时,是否可以清理对应对象。

1. 强引用(Strong Reference)
这是最常见的引用方式。
例如:
java
User user = new User();
变量 user 对对象形成强引用。
只要对象仍然通过强引用链从 GC Roots 可达,垃圾收集器就不会回收它。
日常开发中绝大多数对象都是通过强引用持有的。
2. 软引用(Soft Reference)
软引用的特点是:
当内存比较充足时,GC 通常会保留软引用对象;当内存紧张时,GC 可以清除软引用。
示例:
java
User user = new User();
SoftReference<User> softReference =
new SoftReference<>(user);
软引用曾经常用于内存敏感型缓存。
不过要注意,软引用并不适合用来精确控制缓存容量和过期时间,因为何时清理由 JVM 的内存压力和实现策略决定。
3. 弱引用(Weak Reference)
弱引用比软引用更容易被回收。
如果一个对象只有弱引用关联,在 GC 处理弱引用时,对象就可以被清除。
示例:
java
User user = new User();
WeakReference<User> weakReference =
new WeakReference<>(user);
弱引用常见的应用场景之一是 WeakHashMap。
需要注意:弱引用并不意味着对象创建后立刻消失,而是在垃圾收集器处理弱引用时可以被清除。
4. 虚引用(Phantom Reference)
虚引用是四种引用中最特殊的一种。
它有两个重要特点:
- 无法通过
get()获取对象,调用始终返回null。 - 通常配合
ReferenceQueue使用,在对象达到虚可达状态并被清除后,可以通过队列获知相关事件。
示例:
java
ReferenceQueue<User> queue =
new ReferenceQueue<>();
User user = new User();
PhantomReference<User> phantomReference =
new PhantomReference<>(user, queue);
虚引用主要用于对象回收过程中的通知和资源清理协调。
实际开发中,可以了解 Cleaner 等更现代的资源清理机制,不建议自己依赖 finalize()。
八、有了 GC,为什么还会出现内存泄漏?
很多人会认为:
Java 有垃圾回收器,所以不会发生内存泄漏。
这是错误的。
GC 判断对象是否可以回收,依据的是对象是否可达,而不是业务是否还需要它。
例如:
java
public class Cache {
private static final Map<String, Object> CACHE =
new HashMap<>();
public static void add(String key, Object value) {
CACHE.put(key, value);
}
}
假设我们不断向 CACHE 中添加对象,但从来不删除。
即使业务已经不再使用这些对象,它们仍然被静态 Map 引用。
从 GC 的角度看:
text
GC Roots
|
v
静态字段 CACHE
|
v
Map
|
v
缓存对象
对象仍然可达,GC 就不会回收它们。

这就是为什么 Java 仍然会出现内存泄漏。
常见场景包括:
- 静态集合持续保存不再使用的数据。
- 监听器注册后没有及时注销。
- ThreadLocal 使用不当,导致对象被线程长期持有。
- 缓存缺少容量限制和过期淘汰策略。
因此:
有 GC 不代表不会内存泄漏。只要无用对象仍然被强引用链持有,GC 就无法回收它。
九、GC 是如何触发的?
GC 并不是只有在堆内存完全用完时才会触发。
不同垃圾收集器会根据当前内存状态、对象分配速度、回收成本等因素决定何时进行垃圾回收。
常见场景包括:
1. 对象分配空间不足
例如 Eden 区空间不足,分配新对象时可能触发 Young GC。
2. 达到收集器的启动阈值
例如 G1 会结合老年代占用、对象分配速度和并发标记所需时间等信息,决定何时启动并发标记。
3. 元空间达到相关阈值
元空间压力可能触发垃圾回收尝试,以便回收无用类的元数据。
4. 显式请求 GC
例如:
java
System.gc();
这只是向 JVM 提出一次垃圾回收请求,并不保证一定立即执行,也不保证一定执行 Full GC。
可以通过:
text
-XX:+DisableExplicitGC
配置忽略显式 GC 请求。
因此,不能简单地认为 GC 只有一个固定触发条件。具体行为要结合 JVM 版本、垃圾收集器和运行参数分析。
十、本篇总结
- Java GC 主要负责判断对象是否存活,并回收可以回收的对象所占用的内存。
- HotSpot JVM 主要使用可达性分析判断对象是否存活,而不是单纯使用引用计数法。
- 引用计数法无法解决循环引用问题。
- 可达性分析从 GC Roots 出发,通过对象引用链判断对象是否可达。
- 常见 GC Roots 包括活动线程栈中的引用、存活类的静态字段引用、JNI 引用和 JVM 内部引用等。
- 对象不可达意味着具备被回收的条件,不代表立即回收。
- Java 有强引用、软引用、弱引用和虚引用四种引用类型。
finalize()不可靠且已被弃用,不应该用于资源管理。- Java 仍然可能发生内存泄漏,因为无用对象也可能被静态集合、ThreadLocal 等强引用长期持有。
System.gc()只是请求,不保证立即执行或一定触发 Full GC。
十一、本篇面试题与参考答案
1. 什么是 Java 垃圾回收?GC 主要解决什么问题?
参考答案:
垃圾回收是 JVM 自动管理对象内存的一种机制。它主要负责判断对象是否仍然存活,并回收已经不再需要保留的对象所占用的内存。
它减少了开发者手动管理对象内存的负担,但不能解决所有内存问题。例如静态集合长期持有无用对象时,对象仍然可达,GC 就不会回收它。
2. Java 如何判断一个对象是否可以被回收?
参考答案:
Java 主要通过可达性分析判断对象是否存活。
JVM 从 GC Roots 出发,沿着对象之间的引用关系向下搜索。如果一个对象无法从 GC Roots 到达,就会被判定为不可达对象,具备被回收的条件。
3. 什么是 GC Roots?常见的 GC Roots 有哪些?
参考答案:
GC Roots 是 JVM 进行可达性分析时的起点。常见来源包括活动线程栈帧中的局部变量引用、存活类的静态字段引用、JNI 局部和全局引用,以及 JVM 内部维护的相关引用。
JVM 从这些根节点出发,遍历对象引用关系,判断对象是否可达。
4. 引用计数法有什么缺点?Java 为什么主要使用可达性分析?
参考答案:
引用计数法通过维护对象的引用数量判断对象是否可以回收,但它无法处理循环引用。
例如 A 引用 B,B 又引用 A,即使外部已经没有任何引用,两个对象的引用计数仍然不为零。
可达性分析从 GC Roots 出发判断对象是否可达,即使对象之间存在循环引用,只要没有从 GC Roots 到达它们的引用链,就可以被判定为不可达。
5. 对象不可达就一定会立即被回收吗?
参考答案:
不一定。对象不可达只是说明它具备了被回收的条件,实际什么时候回收由垃圾收集器的运行时机和回收策略决定。
另外,如果对象重写了 finalize() 且此前没有执行过,JVM 可能安排它执行。对象甚至可能在其中重新建立引用,让自己复活。不过 finalize() 已被弃用,不应该依赖它管理对象生命周期。
6. Java 四种引用类型有什么区别?
参考答案:
强引用是最常见的引用,只要强引用链可达,对象就不会被回收。
软引用在内存紧张时可以被清除,适合了解内存敏感型缓存场景,但不适合精确控制缓存策略。
弱引用比软引用更容易被清除,当 GC 处理弱引用时,只有弱引用关联的对象可以被清除。
虚引用无法通过 get() 获取对象,通常配合 ReferenceQueue 使用,用于对象清理过程中的通知和协调。
7. 有了垃圾回收机制,为什么 Java 还会出现内存泄漏?
参考答案:
因为 GC 判断对象是否可以回收,依据的是对象是否从 GC Roots 可达,而不是业务是否还需要它。
例如静态 Map 长期保存不再使用的数据,这些对象仍然能从 GC Roots 到达,因此 GC 不会回收它们。随着对象不断增加,就可能导致堆内存不足。
8. System.gc() 一定会触发 Full GC 吗?
参考答案:
不一定。System.gc() 只是向 JVM 提出显式垃圾回收请求,具体是否执行、采用什么回收方式,取决于 JVM 和垃圾收集器配置。
例如使用 -XX:+DisableExplicitGC 可以忽略显式 GC 请求。因此不能认为调用 System.gc() 就一定会立即执行 Full GC。
9. GC 是在什么时候触发的?
参考答案:
GC 没有一个适用于所有收集器的固定触发条件。常见情况包括对象分配空间不足、收集器达到并发回收启动阈值、元空间达到相关阈值,以及程序显式请求 GC。
具体需要结合当前使用的垃圾收集器、JVM 参数和 GC 日志判断。