Java虚拟机的垃圾对象判定
什么是垃圾对象
在 Java 虚拟机(JVM)中,垃圾对象是指堆内存中通过可达性分析算法判定、不存在任何从 GC Roots(垃圾收集根节点)出发的存活引用链可达的 Java 对象。这类对象在程序后续运行过程中无法被任何合法路径访问和使用,其占用的堆内存若长期未被释放,会持续消耗内存资源,最终可能引发内存溢出(OutOfMemoryError),因此是垃圾收集器(GC)的核心回收目标。
对象的可触及性状态
常规认知中,无 GC Roots 引用链可达的对象看似已无使用价值、应当被回收,但实际这类对象可能通过 finalize() 方法重新建立到 GC Roots 的引用链(即 "复活")。若 JVM 不考虑这种复活可能性,直接将暂时不可达的对象回收,会导致本可被复活复用的对象被销毁,进而引发程序逻辑错误(如空指针异常、资源访问失效、数据不一致等)。
为此,JVM 并非简单以 "是否可达" 作为回收判定的唯一标准,而是通过定义 "可触及的、可复活的、不可触及的" 三级可触及性状态,明确了只有进入 "不可触及" 状态(finalize()已执行且未复活,无任何复活可能)的对象,才是真正安全可回收的。
这也是研究对象复活的核心价值:它是 JVM 设计严谨的垃圾回收判定规则的关键前提,确保 GC 在释放内存的同时,不会因忽略复活场景导致程序运行异常。
对象的三种可触及性状态:
- 可触及的:存在从 GC Roots 出发的有效引用链指向该对象,程序可正常访问和使用该对象。
- 可复活的:所有从 GC Roots 出发的引用链已断开(对象暂时不可达),但该对象的finalize()方法尚未被执行(且未被标记为 "已执行",可通过finalize()方法重新建立引用链实现复活。
- 不可触及的:该对象的 finalize() 方法已被 JVM 执行完毕(JVM 对每个对象仅调用一次finalize()),且未在方法中实现复活,此时对象无任何复活可能,可进行安全回收。
在我后面的博客里会仔细介绍 finalize() 方法的执行流程等内容,这里我们先关注 finalize() 方法复活对象的效果
如下代码:
java
public class CanReliveObj {
public static CanReliveObj obj;
/**
* finalize
* @throws Throwable
*
* finalize只能被调用一次
*/
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("CanRelive0bj finalize called");
//对象的复活
obj = this;
}
@Override
public String toString() {
return "I am CanRelive0bj";
}
public static void main(String[] args) throws InterruptedException {
obj = new CanReliveObj();
obj = null;
System.gc();
Thread.sleep(1000);
if (obj == null) {
System.out.println("obj是null");
} else {
System.out.println("obj可用");
System.out.println("第2次gc");
//继续把引用设置成null,让对象编程匿名对象
obj = null;
System.gc();
Thread.sleep(1000);
if (obj == null) {
System.out.println("obj是null");
} else {
System.out.println("obj可用");
}
}
}
}
运行结果

这个输出完整对应了对象从 可触及→可复活→可触及→不可触及→被回收 的状态变迁:
obj = new CanReliveObj();静态变量 obj 指向新创建的对象,该对象存在从 GC Roots(静态变量)出发的引用链,处于可触及状态;obj = null切断了原对象与 GC Roots 的引用链,对象进入可复活状态;System.gc()触发 GC 后,JVM 检测到该对象可复活,将其放入F-Queue队列,由 Finalizer 线程执行finalize()方法;finalize()执行:this 引用(指向当前对象)被赋值给静态变量obj,重建了对象到 GC Roots 的引用链,对象从可复活状态回到可触及状态,因此后续判断 obj != null;obj = null再次切断引用链后,JVM 检测到该对象的 finalize() 已执行过(JVM 对每个对象仅调用一次finalize()),因此直接标记为不可触及状态,System.gc()最终被 GC 回收。
finalize() 是 Object 类提供的对象回收前的回调方法,但该机制存在设计层面的固有缺陷,因此在生产环境中被明确列为不推荐使用的资源释放方式。
finalize() 方法的核心缺陷:
- 易引发对象意外复活与内存泄漏:
finalize() 方法执行时,当前对象的this引用仍有效,若在方法内将this赋值给 GC Roots 可达的变量,会导致已判定为可复活的对象重新建立引用链,造成对象意外复活,这种非预期的复活会让对象长期占用堆内存,引发内存泄漏;即使无主观的引用外泄操作,也可能因代码疏漏导致类似问题,破坏内存管理的确定性。 - 执行机制缺陷:
- 执行时机不可控:finalize() 由 JVM 的低优先级 Finalizer 线程异步执行,GC 仅负责将待处理对象放入 F-Queue 队列,不保证该方法的执行时间,甚至不保证一定会执行(如 JVM 进程异常终止),无法满足资源释放的及时性要求;
- 阻塞 GC 流程:若 finalize() 执行耗时会阻塞 Finalizer 线程,导致 F-Queue 队列堆积,进而延缓整个 GC 周期,降低垃圾回收效率;
- 仅执行一次:JVM 对每个对象仅调用一次 finalize(),若方法执行失败,既不会重试也不会反馈,无法保证资源释放的可靠性。
推荐释放资源方案:
- try-with-resources:
- 核心原理:基于java.lang.AutoCloseable接口(或其子接口Closeable)实现,将资源对象声明在try()括号内,JVM 会在try代码块执行完毕后(无论正常结束还是抛出异常),自动且同步地调用资源的close()方法,确保资源确定性释放。
- 适用场景:文件 IO、数据库连接、网络套接字等需要显式关闭的资源(实现了AutoCloseable接口的资源类)。
- 显式调用close():
- 核心原理:手动调用资源的close()方法,并将调用逻辑放在finally块中(finally块保证无论try块是否抛出异常,都会执行),确保资源释放逻辑不被跳过。
- 适用场景:JDK 7 以下版本、无法使用 try-with-resources 的自定义资源管理场景。
- java.lang.ref.Cleaner:
- 核心原理:JDK 9 引入的官方替代方案,基于虚引用(PhantomReference) 实现,无需依赖 Finalizer 线程,由 JVM 在对象被判定为不可触及后,异步触发清理逻辑,避免了finalize()的阻塞 GC、执行时机不可控等缺陷。
- 适用场景:需要在对象被 GC 回收时触发资源清理,且无法保证用户显式调用close()的场景(如通用组件开发)。
- PhantomReference(虚引用)+ReferenceQueue------监控对象回收:
- 核心原理:持有虚引用不会影响对象的 GC 回收(即使有虚引用指向对象,只要无其他可达引用,对象仍会被判定为不可触及),对象被 GC 回收后,对应的虚引用会被 JVM 加入 ReferenceQueue 队列,通过监听队列可触发自定义资源清理逻辑。
- 适用场景:需要深度定制对象回收后的清理逻辑,或基于低版本 JDK 实现类似 Cleaner 的功能。
不同引用类型的垃圾判定规则
Java 通过四种引用类型精准控制对象的可达性,引用类型决定了对象可触及性的强度,而可触及性强度直接定义了 GC 回收该对象的时机和规则。各引用类型的核心特性与判定规则如下表所示:
| 引用类型 | 垃圾判定规则 |
|---|---|
| 强引用 | 只要存在强引用指向对象,无论内存是否充足,对象始终为可触及状态,不会被 GC 回收 |
| 软引用 | 内存充足时对象为可触及状态,内存不足时对象被判定为垃圾并优先回收 |
| 弱引用 | 无论内存是否充足,只要触发 GC,仅被弱引用关联的对象会从弱可触及状态直接变为不可触及状态,立即被回收 |
| 虚引用 | 虚引用不影响对象可触及性,仅关联虚引用的对象始终为不可触及状态;需与ReferenceQueue配合使用,仅用于跟踪对象回收状态 |
除强引用外,其他三种引用的包装类均可以在 java.lang.ref 包中找到,如图所示继承体系如下:

其中 FinalReference(译为 "最终引用")是的抽象类,它被标记为 package-private(包级私有),因此开发者无法直接使用、继承或实例化该类,仅由 JDK 内部实现调用,其核心作用是关联待执行 finalize () 方法的对象。
Java 中对象的 finalize() 方法并非由开发者主动调用,而是由 JVM 的 Finalizer 线程负责触发,FinalReference 正是 JVM 用来跟踪 "需要执行 finalize () 方法的对象" 的核心载体,具体流程为:
- 当一个对象被 GC 标记为可回收,且该对象重写了 finalize() 方法、该方法从未被执行过时,JVM 会为该对象创建一个 FinalReference 实例,并将其加入 Finalizer 队列;
- JVM 内置的 Finalizer 守护线程会持续从 Finalizer 队列中取出 FinalReference 实例,调用其关联对象的 finalize() 方法;
- finalize() 方法执行完成后,对应的 FinalReference 实例会被清理,该对象才会在后续 GC 中被真正回收(若未在finalize()中被复活)。
下面逐一介绍强引用 、软引用、弱引用和虚引用。
强引用
强引用(Strong Reference)是 Java 中最基础、使用最广泛的引用类型,指无需借助任何引用类包装、仅通过直接赋值语句建立的对象关联关系;若一个对象存在从 GC Roots 出发的可达强引用链,JVM 会判定该对象处于 "可触及状态",并将其认定为程序运行所必需的对象,不会将其作为垃圾回收。
示例:
java
StringBuffer str = new StringBuffer("hello world");
假设以上代码是在函数体内运行的,那么局部变量 str 将被分配在栈上,而对象 StringBuffer 实例被分配在堆上。局部变量 str 指向 StringBuffer 实例所在堆空间,通过 str 可以操作该实例,那么 str 就是 StringBuffer 实例的强引用。
再加一条语句:
java
StringBuffer str1 = str;
那么,str 所指向的对象也将被 str1 所指向,同时在局部变量表上会分配空间存放 str1 变量。此时,该 StringBuffer 实例就有两个引用。对引用的 "==" 操作用于表示两操作数所指向的堆空间地址是否相同。
从内存布局来看:
局部变量存储在虚拟机栈的栈帧局部变量表中,引用指向的对象实例存储在堆中;
栈中变量持有堆中对象的内存地址,通过该地址可直接操作目标对象,这也正是强引用的本质。
强引用的核心特性:
- 直接访问目标对象
强引用可直接调用对象的方法、访问成员变量,是程序操作对象的最直接方式。 - 优先级最高
在 Java 的四种引用类型中,强引用优先级远超其他引用。即使对象同时被强引用和其他引用关联,JVM 也会以强引用为准,判定对象为可触及状态。 - 永不被 GC 主动回收
只要指向对象的强引用存在,且该引用属于可达的 GC Roots,无论 JVM 内存是否充足,GC 都不会将该对象标记为垃圾并回收;极端情况下,JVM 宁愿抛出OutOfMemoryError(OOM)终止程序,也不会回收强引用关联的对象。 - 构成 GC Roots 的核心组成
强引用是 GC Roots(垃圾回收根节点)的主要来源,只有属于 GC Roots 的强引用才会阻断对象回收,常见的 GC Roots 强引用包括:- 虚拟机栈(栈帧局部变量表)中的强引用(如方法内的局部变量obj);
- 方法区中类静态属性的强引用(如public static Object STATIC_OBJ = new Object());
- 本地方法栈中 JNI(Native 方法)的强引用(如调用 C++ 方法时传递的 Java 对象引用);
- JVM 内部的核心对象(如系统类加载器、未关闭的 Socket)。
强引用关联对象的回收触发条件
仅当满足所有以下条件时,强引用关联的对象才会被 GC 判定为 "不可触及状态",并在后续 GC 周期中被回收:
- 解除所有可达的强引用:
显式将指向该对象的强引用赋值为null,主动切断引用关联;
强引用变量超出其作用域(如方法执行完毕导致栈帧销毁,局部变量表中的强引用随之消失)。 - 无其他可达的强引用:
该对象未被任何属于 GC Roots 的强引用(如静态变量、存活对象的成员变量)或 GC Roots 可达的强引用间接关联。 - 未复活:
即使对象重写了 finalize () 方法,也未在该方法执行过程中重新建立与 GC Roots 的强引用链;假设对象在 finalize () 中完成复活,那么会重新回到 "可触及状态",无法被 GC 回收。
强引用是引发 JVM 内存泄漏的首要原因:
当本该被 GC 回收的对象,因被意外保留的可达强引用持续持有,会导致对象始终处于 "可触及状态" 而无法被回收,长期占用堆内存;随着此类对象不断累积,堆内存占用持续升高,最终触发 OutOfMemoryError(OOM)异常。
软引用
软引用(Soft Reference)是由 java.lang.ref.SoftReference 类封装的引用类型,优先级介于强引用与弱引用之间。其核心语义为:若对象仅被软引用关联(无任何可达的强引用),JVM 会判定该对象处于软可触及状态 ------ 内存充足时,该对象会被保留,不会被 GC 回收;仅当堆内存不足、即将抛出OutOfMemoryError(OOM)异常时,JVM 会将其标记为垃圾对象并回收,以此避免内存溢出。
下面示例演示了软引用会在系统堆内存不足时被回收的情况
java
import java.lang.ref.SoftReference;
public class SoftRef {
public static class User {
public int id;
public String name;
// 让User对象占用可量化的内存
private byte[] data = new byte[1024 * 1024 * 5]; // 每个User占5MB内存
public User(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return " [id=" + String.valueOf(id) + ", name=" + name + "]";
}
}
public static void main(String[] args) {
User u = new User(1, "geym");
//创建u的软引用
SoftReference<User> userSoftRef = new SoftReference<User>(u);
//让我们的User对象变成匿名对象
u = null;
//通过软引用来获得对象
System.out.println("初始状态:" + userSoftRef.get());
//调用垃圾回收器 ,由于软引用的存在,对象没有被回收
System.gc();
System.out.println("After GC:");
System.out.println(userSoftRef.get());
// 循环分配内存,确保耗尽堆空间
try {
// 存储大数组,避免被GC回收,持续占用内存
byte[][] largeArrays = new byte[10][];
for (int i = 0; i < 10; i++) {
largeArrays[i] = new byte[1024 * 1024 * 2]; // 每个数组2MB
System.out.println("分配第" + (i+1) + "块大内存后,软引用对象:" + userSoftRef.get());
// 软引用已回收则终止循环
if (userSoftRef.get() == null) break;
}
} catch (OutOfMemoryError e) {
// 捕获OOM,验证软引用已回收
System.out.println("触发OOM前,软引用对象:" + userSoftRef.get());
}
// 再次触发GC,确保回收逻辑执行
System.gc();
System.out.println("分配大内存后GC:" + userSoftRef.get());
}
}
首先定义了一个User内部类,为了让该类实例占用足够内存在类中添加了一个占用 5MB 内存的byte数组,同时保留了 id、name 属性和 toString 方法用于标识对象;在 main 方法中,先创建一个 User 对象并通过强引用u指向它,接着用SoftReference创建该对象的软引用userSoftRef,随后将强引用u赋值为 null,使 User 对象仅被软引用关联;此时内存充足,手动触发 GC 后,因内存仍充足,软引用关联的对象不会被回收;为模拟内存不足场景,代码通过 try 块循环分配大数组 ------ 创建长度为 10 的二维字节数组largeArrays,每次循环分配 2MB 的字节数组并存入其中,持续占用堆内存,同时实时打印软引用状态,当堆内存耗尽时,JVM 为避免 OOM 会主动回收仅被软引用关联的 User 对象,此时userSoftRef.get()返回 null,循环会提前终止;若分配内存过程中触发 OOM 异常,catch 块会捕获并打印软引用状态,验证回收行为。最后再次触发 GC,确认软引用对象已被回收并打印 null。
使用参数 -Xms20M -Xmx20M -XX:+PrintGCDetails 运行上述代码,执行结果如下

从该示例可得出核心结论:GC 并不会在内存充足时回收仅被软引用关联的对象,仅当 JVM 堆内存资源紧张、即将抛出 OutOfMemoryError(OOM)时,才会优先回收这类软引用对象;因此合理使用软引用,能有效利用内存做缓存,且不会因软引用本身引发内存溢出。
弱引用
弱引用(Weak Reference)是一种优先级低于强引用和软引用的引用类型,也是 Java 中存活周期最短的非强引用类型。它由 java.lang.ref.WeakReference 类封装实现,核心语义为:若对象仅被弱引用关联,无论 JVM 堆内存是否充足,只要触发 GC,该对象会被立即标记为垃圾并回收。
需要注意的是,垃圾回收器的线程通常优先级较低,GC 的触发时机并非即时可控,因此仅被弱引用关联的对象可能不会被立刻回收,而是会在 GC 真正执行前的一段时间内依然存在;但从规则上看,一旦 GC 开始执行,这类对象必然会被回收,其生命周期本质上仅能持续到下一次 GC 触发前。
看下面的例子
java
import java.lang.ref.WeakReference;
public class WeekRef {
public static class User {
public int id;
public String name;
public User(int id, String name) {
this.id = id;
this.name = name;
}
@Override
public String toString() {
return " [id=" + String.valueOf(id) + ", name=" + name + "]";
}
}
public static void main(String[] args) {
User u = new User(1, "geym");
WeakReference<User> userWeakRef = new WeakReference<User>(u);
u = null;
System.out.println(userWeakRef.get());
System.gc();
//不管当前内存空间足够与否,都会回收它的内存
System.out.println("After GC:");
System.out.println(userWeakRef.get());
}
}
运行上述代码,输出为:

在main方法中,先创建User对象并通过强引用u指向它,接着创建弱引用userWeakRef关联该User对象(此时对象同时被强、弱引用关联,弱引用特性暂不生效),随后将强引用u赋值为null(切断强引用关联,使User对象仅被弱引用关联,弱引用特性开始生效);此时未触发 GC,通过userWeakRef.get()能获取到User对象;接着调用System.gc()建议 JVM 执行垃圾回收,最后再次调用userWeakRef.get(),由于 GC 已执行,仅被弱引用关联的User对象已被回收,因此打印结果为null,清晰验证了弱引用 "无论内存是否充足,只要触发 GC 就会回收仅被其关联的对象" 的规则。
软引用、弱引用均适合存储可有可无的缓存数据:其中软引用存储的缓存数据,会在 JVM 内存不足、即将抛出 OOM 时被回收,内存充足时则可长期保留,既能避免内存溢出,又能利用缓存加速系统;弱引用存储的缓存数据,无论内存是否充足,只要触发 GC 就会被回收,适合存储 "无需长期保留、可随时重建" 的临时缓存,虽无法长期留存,但也不会因缓存堆积导致内存溢出。
虚引用
虚引用(Phantom Reference)是 Java 中最弱的引用类型,由 java.lang.ref.PhantomReference 类实现。它的核心特点是无法通过虚引用获取对象实例,仅能用于跟踪对象被 GC 回收的时机。
虚引用的优先级低于强引用、软引用和弱引用,其核心语义为:
虚引用必须与 ReferenceQueue(引用队列)配合使用,创建时必须指定关联的队列。
无论内存是否充足,只要对象仅被虚引用关联,就会在 GC 时被回收。
虚引用的get()方法始终返回null,无法通过它获取对象实例,这是它与其他引用类型最根本的区别。
对象被 GC 回收后,其关联的虚引用会被自动放入ReferenceQueue,以通知应用程序对象的回收情况。
此外,虚引用关联的对象在被 GC 标记为可回收后,即使重写了 finalize() 方法也无法复活,因为虚引用的设计目标就是确保对象能够被彻底、不可逆转地回收。
java
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
public class TraceCanReliveObj {
public static TraceCanReliveObj obj;
//定义引用队列
static ReferenceQueue<TraceCanReliveObj> phantomQueue = null;
public static class CheckRefQueue extends Thread {
@Override
public void run() {
while (true) {
if (phantomQueue != null) {
PhantomReference<TraceCanReliveObj> objt = null;
try {
//从引用队列中移除元素并接收
objt = (PhantomReference<TraceCanReliveObj>) phantomQueue.remove();
} catch (InterruptedException e) {
e.printStackTrace();
}
//能接收到说明垃圾回收器回收了对象
if (objt != null) {
System.out.println("TraceCanReliveObj is delelte by GC");
}
}
}
}
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("CanRelive0bj finalize called");
obj = this;
}
@Override
public String toString() {
return "I am CanRelive0bj";
}
public static void main(String[] args) throws InterruptedException {
//创建并且启动线程
Thread t = new CheckRefQueue();
t.setDaemon(true);
t.start();
//实例化引用队列
phantomQueue = new ReferenceQueue<TraceCanReliveObj>();
//创建强引用对象
obj = new TraceCanReliveObj();
//创建虚引用对象,虚引用对象, 但是还没有进入到队列
PhantomReference<TraceCanReliveObj> phantomRef = new PhantomReference<TraceCanReliveObj>(obj, phantomQueue);
obj = null;
System.gc();
Thread.sleep(1000);
if (obj == null) {
System.out.println("obj是null");
} else {
System.out.println("obj可用");
}
System.out.println("第2次gc");
obj = null;
System.gc();
Thread.sleep(1000);
if (obj == null) {
System.out.println("obj是null");
} else {
System.out.println("obj可用");
}
}
}

TraceCanReliveObj 类重写了 finalize() 方法,方法内将当前对象赋值给静态变量obj以尝试复活对象,同时定义守护线程 CheckRefQueue,该线程持续监听虚引用队列 phantomQueue,一旦从队列中获取到虚引用实例,就打印对象被 GC 回收的提示。
在main方法中,先启动该守护线程并初始化引用队列,创建 TraceCanReliveObj 实例让静态变量持有强引用,接着创建该对象的虚引用并关联到引用队列,随后将强引用置为null(切断强引用关联,使其仅被弱引用关联);第一次调用 System.gc() 触发 GC 时,对象因仅被虚引用关联被标记为可回收,JVM 调用其 finalize() 方法,对象复活,因此 GC 无法回收该对象;第二次触发 GC 时,由于 finalize() 方法在对象生命周期中仅能被调用一次,对象无法再次复活,最终被 GC 回收,其关联的虚引用被加入引用队列,守护线程检测到后打印 "TraceCanReliveObj is delelte by GC",完整验证了虚引用需配合引用队列使用、对象仅能通过finalize()复活一次的核心特性。