Java的垃圾回收(GC)机制通过判断对象是否存活来决定是否回收其占用的内存,而对象引用和引用链是GC判断对象存活的核心依据。GC扫描过程中会针对不同强度的引用采取不同的处理策略,同时通过遍历引用链确认对象是否可达。
Java中的四种对象引用类型
Java根据引用强度将对象引用分为四类,GC扫描时对不同引用的处理方式存在明显差异:
- 强引用:最常见的引用形式,如
Object obj = new Object(),只要强引用存在,GC永远不会回收被引用的对象。 - 软引用:通过
SoftReference类实现,内存充足时不会回收,内存不足时才会被GC回收。 - 弱引用:通过
WeakReference类实现,无论内存是否充足,只要发生GC扫描到该引用,就会回收对应的对象。 - 虚引用:通过
PhantomReference类实现,完全不影响对象生命周期,唯一作用是在对象被回收时收到系统通知。
GC扫描中不同引用的处理规则
GC触发后首先会进行根节点枚举,找到所有GC Roots节点,之后按照引用类型处理关联的对象:
强引用处理
GC扫描到强引用时,会直接标记该引用指向的对象为存活状态,不会将其纳入回收候选集合。只有当强引用被显式赋值为null,或者持有强引用的对象本身被回收后,对应的对象才会失去强引用关联。
软引用处理
GC扫描到软引用时,不会立即回收其指向的对象,只有当堆内存使用率达到阈值,即将发生内存溢出时,才会将软引用指向的对象标记为可回收。如果对象被软引用关联的同时还被强引用关联,则不会被回收。
弱引用处理
弱引用的存活周期仅到下一次GC发生为止,GC扫描过程中只要发现弱引用,不管当前内存是否充足,都会将弱引用指向的对象标记为可回收。弱引用本身需要配合引用队列使用,对象被回收后弱引用会被放入引用队列中。
虚引用处理
虚引用对对象的生命周期没有任何影响,GC扫描时不会根据虚引用判断对象是否存活。虚引用必须和引用队列联合使用,当对象被GC回收时,虚引用会被加入到关联的引用队列中,程序可以通过判断引用队列中是否有虚引用来得知对象是否已被回收。
引用链的构建逻辑
引用链是从GC Roots节点出发,通过引用关系串联起来的对象路径,GC通过遍历引用链判断对象是否可达:
GC Roots节点的组成
可以作为GC Roots的节点包括以下几类:
- 虚拟机栈中局部变量表引用的对象
- 本地方法栈中JNI引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 被同步锁持有的对象
引用链的构建过程
引用链的构建以GC Roots为起点,逐个遍历每个节点关联的其他对象,将所有可达的对象串联起来形成引用链。如果某个对象不在任何一条从GC Roots出发的引用链上,就说明该对象不可达,可以被回收。
以下是一个简单的引用链示例:
public class ReferenceChainDemo {
private static Object staticObj = new Object(); // 静态属性引用,属于GC Roots
private Object instanceObj; // 实例属性引用
public static void main(String[] args) {
ReferenceChainDemo demo = new ReferenceChainDemo(); // 局部变量引用,属于GC Roots
demo.instanceObj = new Object(); // demo的实例属性引用instanceObj指向的对象
// 此时引用链为:main方法局部变量demo -> ReferenceChainDemo对象 -> instanceObj属性 -> Object对象
// 同时staticObj也是GC Roots直接指向一个Object对象,形成另一条引用链
}
}
引用链的遍历机制
GC遍历引用链的过程通常采用可达性分析算法,主流的遍历实现方式有两种:
深度优先遍历
从GC Roots节点出发,沿着一条引用路径一直遍历到末端,再回溯遍历其他分支。这种方式的实现相对简单,不需要额外存储太多临时数据,是很多GC实现默认采用的遍历方式。
广度优先遍历
从GC Roots节点出发,先遍历所有直接关联的对象,再逐层遍历这些对象关联的下一级对象。这种方式可以更快速地找到距离GC Roots最近的对象,适合需要优先处理短引用链的场景。
遍历过程中GC会维护一个已标记对象的集合,避免重复遍历同一个对象。如果遍历过程中遇到软引用、弱引用等特殊情况,会按照对应引用的处理规则调整对象的存活状态标记。
引用处理与引用链遍历的示例
以下代码演示了不同引用类型在GC扫描和引用链遍历中的表现:
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
public class GCReferenceDemo {
public static void main(String[] args) {
// 强引用
Object strongRef = new Object();
// 软引用
SoftReference<Object> softRef = new SoftReference<>(new Object());
// 弱引用
WeakReference<Object> weakRef = new WeakReference<>(new Object());
// 触发GC
System.gc();
// 强引用对象仍然存在
System.out.println("强引用对象:" + strongRef);
// 内存充足时软引用对象仍存在
System.out.println("软引用对象:" + softRef.get());
// 弱引用对象已被回收
System.out.println("弱引用对象:" + weakRef.get());
// 模拟内存不足场景
List<byte[]> list = new ArrayList<>();
try {
while (true) {
list.add(new byte[1024 * 1024]); // 不断占用内存
}
} catch (OutOfMemoryError e) {
// 内存不足时软引用对象被回收
System.out.println("内存不足时软引用对象:" + softRef.get());
}
}
}
上述代码中,触发GC后弱引用指向的对象会立即被回收,而软引用对象在内存充足时仍然存在,只有当内存不足发生OOM前才会被回收,强引用对象则始终不会被回收。
常见注意事项
- 避免使用无意义的强引用持有大对象,容易导致内存泄漏,比如将大对象放入静态集合且不及时清理。
- 缓存场景优先使用软引用或弱引用,避免缓存对象占用过多内存无法被回收。
- 引用队列需要和软引用、弱引用、虚引用配合使用,才能及时感知对象被回收的事件,避免引用本身成为内存泄漏点。
- GC扫描和引用链遍历的过程会暂停用户线程(STW),因此合理减少对象引用层级可以缩短遍历时间,降低STW的影响。