1. 前言
在 Java 开发过程中,我们常常听到“垃圾回收”(Garbage Collection, GC)这一术语。JVM 通过垃圾回收机制自动管理内存,极大地简化了程序员的内存控制负担。然而,GC 究竟是如何判断哪些对象该回收、哪些应保留的呢?这正是“对象存活判定”的关键所在。
对象存活判定方法的效率和准确性,直接关系到系统性能的高低。在内存紧张的场景中,一个不合理的回收策略可能导致频繁 GC,甚至导致 OutOfMemoryError(OOM)错误。
Java 语言自诞生之初就非常重视内存安全问题,JVM 也随着版本更新不断优化垃圾回收算法。目前主流的 Java 8 就采用了更加高效的“可达性分析法”替代传统的“引用计数法”,以规避引用循环等典型问题。
本系列文章将从基础原理入手,深入剖析 JVM 如何判断对象是否“还活着”,并配合图解与示例代码,帮助开发者更好地理解这一 GC 背后的核心机制。
阅读后你将收获:
-
JVM 内存模型与对象管理原理
-
引用计数法与可达性分析法的差异与优劣
-
如何用代码分析对象是否会被 GC 回收
-
实用工具(MAT、JVisualVM)的分析技巧
让我们从最基础的问题开始——“对象存活判定”到底指的是什么?
2. 什么是对象存活判定?
对象存活判定(Object Liveness Detection)是指 JVM 在垃圾回收过程中判断某个对象是否仍“有用”的一套逻辑机制。只有当 JVM 确认一个对象“无用”时,才会将其内存空间释放。
那么,什么是“有用”或“无用”?这个标准并非由程序员显式指定,而是 JVM 通过一定的算法推导得出。
从 JVM 的角度来看:
-
“有用”的对象:程序仍然可以访问到该对象。
-
“无用”的对象:程序中不再有任何方式可以访问到该对象。
这就涉及到两个关键问题:
-
程序如何“访问”一个对象?
-
JVM 如何判断“是否还能访问”?
为了解决这两个问题,JVM 提供了两种主要的判断方式:
-
引用计数法(Reference Counting):为每个对象维护一个引用计数器。
-
可达性分析法(Reachability Analysis):从一组被称为 GC Roots 的起点出发,遍历对象图。
这两种方法各有利弊,也体现了 JVM 垃圾回收策略的演进方向。
在接下来的章节中,我们将逐一剖析这两种算法的底层原理、适用场景与实现机制,并结合示意图和代码说明其工作方式。
3. 方法一:引用计数法
基本原理
引用计数法的核心思想非常直观:
每当有一个地方引用该对象,其引用计数就加 1; 每当有一个引用失效,其引用计数就减 1; 当引用计数为 0 时,说明该对象“无人引用”,可以回收。
这一机制类似于手动管理内存语言(如 C++ 的智能指针),但在 Java 中并未采用这种方式作为主流实现。
示意图
假设我们有以下引用关系:
A --> B --> C^ ||_____|
-
对象 A 引用了 B,B 引用了 C,C 又回头引用了 B,构成循环引用。
-
即便 A 被回收,B 与 C 相互引用,导致引用计数不为 0,从而无法释放。
这就是引用计数法的致命缺陷:无法处理对象之间的循环引用问题。
示例代码
虽然 Java 官方并未公开支持引用计数 GC,但我们可以通过伪代码演示其原理:
class MyObject {int refCount = 0;void addReference() {refCount++;}void removeReference() {refCount--;if (refCount == 0) {// 回收对象内存System.out.println("对象可以被回收");}}
}
使用时:
MyObject obj = new MyObject();
obj.addReference(); // 引用 +1
obj.removeReference(); // 引用 -1,若为0则可回收
当然,实际 JVM 中并未使用这种方法来管理对象生命周期。
优缺点分析
优势 | 劣势 |
---|---|
算法实现简单,效率高 | 无法处理循环引用 |
回收实时性好 | 增加引用维护成本 |
易于实现跨语言互操作 | 与现代 JVM 架构不兼容 |
因此,虽然引用计数法在一些脚本语言(如 Python)或 C++ 的智能指针中应用较多,但在 Java JVM 中并未成为主流方法。
下一节我们将介绍 JVM 真正使用的对象存活判定方式:可达性分析法(Reachability Analysis)。
4. 方法二:可达性分析法
GC Roots 概念
可达性分析法(Reachability Analysis)是目前 Java 虚拟机中对象存活判定的主流算法。
该方法的核心思想是:通过从一组称为 "GC Roots" 的起始节点出发,沿着对象引用链向下搜索,如果某个对象从 GC Roots 出发可达,则说明该对象是“活着”的;否则就会被判定为“死亡”。
GC Roots 的起始节点通常包括:
-
虚拟机栈(栈帧中的本地变量表)中引用的对象
-
方法区中类静态属性引用的对象
-
方法区中常量引用的对象
-
本地方法栈中 JNI 引用的对象
我们将在后续的章节中深入介绍这些 GC Roots 的类型。
分析流程
整个分析过程可以类比成遍历一张“对象图”:
-
建立对象引用图(Object Graph) 所有对象通过引用连接形成有向图,图中的边代表引用关系。
-
标记可达对象 从 GC Roots 出发,标记所有可到达的对象,形成“可达集合”。
-
未被标记的对象即为不可达对象 这些不可达的对象被视为垃圾,等待 GC 清理。
注意:即使对象不可达,JVM 并不会立刻回收它。 如果该对象覆盖了
finalize()
方法,还会进入一次“F-Queue”队列,被 GC 再次确认其是否真的不可用。
图解说明
[GC Roots]|-------------------------| | |Obj1 Obj2 Obj3| |Obj4 Obj5Obj6 (无法从 GC Roots 到达)
-
Obj1~Obj5 均从 GC Roots 可达,为存活对象。
-
Obj6 无任何引用链连接至 GC Roots,被视为“死亡对象”。
示例代码演示
虽然 JVM 自动完成对象图的构建和遍历,我们无法直接干预,但可以通过示例展示“对象是否可达”的效果:
public class ReachabilityDemo {static class Node {String name;Node reference;Node(String name) {this.name = name;}@Overrideprotected void finalize() throws Throwable {System.out.println(name + " 被回收了");}}public static void main(String[] args) {Node a = new Node("A");Node b = new Node("B");Node c = new Node("C");a.reference = b;b.reference = c;a = null; // 去除对 A 的强引用b = null; // 去除对 B 的强引用c = null; // 去除对 C 的强引用System.gc(); // 显式请求 GCtry {Thread.sleep(1000); // 等待 GC 完成} catch (InterruptedException e) {e.printStackTrace();}}
}
输出示例:
C 被回收了
B 被回收了
A 被回收了
说明 A、B、C 都在不可达状态下被 GC 回收。
优势与 JVM 的支持
优点 | 说明 |
---|---|
能解决循环引用问题 | 不依赖引用计数值,识别结构关系 |
更适合复杂对象图 | 图遍历可适配大型堆场景 |
JVM 官方支持 | Java 8 及以后的所有主流 JVM 均基于该方法 |
下一节我们将具体介绍 GC Roots 中的各类节点来源,帮助大家更深入理解对象“可达”的起点到底是什么。
5. GC Roots 的类型
在上一节中我们提到,GC Roots 是可达性分析的起点。那么,GC Roots 到底是什么?哪些对象或引用属于 GC Roots?理解 GC Roots 是掌握 JVM 垃圾回收机制的核心一步。
GC Roots 主要包括以下几种类型的引用:
1. 虚拟机栈中的引用(局部变量表)
每个线程在执行方法时都会创建一个栈帧(Stack Frame),其中的局部变量表中保存着各种基本类型和对象引用。
public class StackReferenceDemo {public static void main(String[] args) {Object obj = new Object(); // obj 是 GC Root 引用System.gc();}
}
在这个例子中,obj
是定义在主方法中的局部变量,它保存在栈帧的局部变量表中,因此是 GC Roots。
2. 方法区中类静态属性引用的对象
静态字段随着类的加载而存在于方法区中,引用的对象也会被视为 GC Roots。
public class StaticReferenceDemo {private static Object staticObj = new Object(); // 属于 GC Rootpublic static void main(String[] args) {System.gc();}
}
即使没有局部变量引用 staticObj
,它依然不会被 GC,因为它是类的静态属性。
3. 方法区中常量引用的对象
常量池中的引用,如字符串常量等,也是 GC Roots 的一部分。
public class ConstantPoolDemo {public static void main(String[] args) {String str = "hello world"; // 字符串常量常驻内存System.gc();}
}
在这个例子中,字符串 "hello world" 常驻在运行时常量池中,是 GC Roots 的一部分,不会被回收。
4. 本地方法栈中的 JNI 引用(Native 引用)
如果 Java 程序调用了本地方法(如 C/C++ 实现的库),这些 native 方法中持有的对象引用也会被当作 GC Roots。
public class JNIDemo {static {System.loadLibrary("native-lib");}public native void callNative();
}
虽然无法用 Java 展示 native 层引用的具体内容,但这些引用 JVM 会在 GC 时特殊处理。
5. 活跃线程
所有运行中的线程(如主线程、GC线程、后台线程等)都是 GC Roots,因为它们自身的引用链天然“存活”。只有当线程执行结束、退出后,它们才会从 GC Roots 移除。
6. JVM 内部保留的系统类加载器
例如 sun.misc.Launcher$AppClassLoader
、ExtClassLoader
等,这些类加载器加载的类及其引用的对象会被视为 GC Roots。
7. JDK 特殊结构
如 System.in/out/err
、线程上下文类加载器、反射中的 Method/Field/Constructor
对象、线程组等。这些结构大多存在于系统级类中,使用时容易导致内存泄露。
总结 GC Roots 类型
GC Roots 类型 | 是否常见 | 是否手动可控 |
---|---|---|
虚拟机栈引用 | ✅ 常见 | ✅ 可控 |
静态属性引用 | ✅ 常见 | ✅ 可控 |
常量池引用 | ✅ 常见 | ❌ 不建议操作 |
JNI 本地引用 | ❗ 复杂 | ❌ 不建议操作 |
活跃线程引用 | ✅ 常见 | ❌ 不可控 |
类加载器引用 | ✅ 常见 | ❌ 不可控 |
系统类结构引用 | ✅ 隐蔽 | ❌ 不可控 |
理解 GC Roots 的种类不仅有助于判断哪些对象能被 GC 回收,也对分析内存泄露、优化引用管理非常有帮助。
在下一节中,我们将进一步探索 Java 中的 finalize()
机制,以及对象“抢救”自己的最后机会。
6. Finalize 机制与固定对象
即使一个对象在 GC Roots 的可达性分析中被判定为“不可达”,也不代表它立刻会被回收。Java 提供了一个“临终遗言”机制,即 finalize()
方法,使对象有一次自我拯救的机会。
6.1 什么是 finalize()
finalize()
是 java.lang.Object
类中的一个方法:
protected void finalize() throws Throwable {// 释放资源或对象复活的钩子方法
}
当对象第一次被判定为不可达时,GC 会检查该对象是否覆盖了 finalize()
方法,且该方法是否尚未被调用。如果满足条件,JVM 会将该对象放入一个名为 Finalization Queue 的队列中,由一个低优先级的 Finalizer 线程去执行其 finalize()
方法。
注意:每个对象的 finalize()
方法最多只会被调用一次。
6.2 finalize() 能做什么?
-
释放资源:用于释放文件句柄、关闭网络连接等非内存资源(但不推荐这么用,推荐使用 try-with-resources)。
-
复活对象:对象在
finalize()
中如果再次赋值给 GC Roots 引用链中的某个变量,则对象会“复活”。
6.3 示例:对象的自我拯救
public class FinalizeRescueDemo {public static FinalizeRescueDemo OBJ = null;@Overrideprotected void finalize() throws Throwable {super.finalize();System.out.println("finalize() 方法被调用");OBJ = this; // 对象复活!}public static void main(String[] args) throws InterruptedException {OBJ = new FinalizeRescueDemo();// 第一次 GC,对象有机会复活OBJ = null;System.gc();Thread.sleep(1000);System.out.println(OBJ != null ? "对象存活" : "对象死亡");// 第二次 GC,finalize() 不会再被调用OBJ = null;System.gc();Thread.sleep(1000);System.out.println(OBJ != null ? "对象存活" : "对象死亡");}
}
运行结果:
finalize() 方法被调用
对象存活
对象死亡
说明:第一次 GC 时 finalize()
被调用,OBJ
被重新引用,从而复活。第二次 GC 时不再执行 finalize()
,对象被真正回收。
6.4 finalize() 的问题与风险
-
不可控时机:执行时间不确定,依赖 GC。
-
影响性能:JVM 要维护一个队列和额外线程。
-
风险隐患:对象复活逻辑可能导致资源泄露或更难以调试的 bug。
-
已被废弃:Java 9 开始标注为
@Deprecated
,建议使用java.lang.ref.Cleaner
替代。
6.5 替代方案:Cleaner
import java.lang.ref.Cleaner;public class CleanerDemo {private static final Cleaner cleaner = Cleaner.create();static class Resource implements Runnable {@Overridepublic void run() {System.out.println("资源被清理");}}public static void main(String[] args) {Object obj = new Object();cleaner.register(obj, new Resource());}
}
Cleaner
提供了比 finalize()
更轻量、可控的资源清理方式,推荐在现代 Java 项目中使用。
7. 不同引用类型与垃圾回收行为
Java Reference类及其实现类深度解析:原理、源码与性能优化实践
Java 中定义了四种不同级别的引用类型:强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference) 和 虚引用(Phantom Reference),它们在 JVM 中表现出不同的“生存权重”。理解这些引用类型对于资源缓存、内存优化和对象生命周期控制至关重要。
7.1 强引用(Strong Reference)
这是最常见的引用类型:
Object obj = new Object();
只要强引用还存在,GC 永远不会回收该对象。
特性:
-
是默认引用类型。
-
会阻止 GC 回收所指向的对象。
示例:
public class StrongReferenceDemo {public static void main(String[] args) {Object obj = new Object();System.gc();System.out.println(obj != null ? "对象未被回收" : "对象被回收");}
}
输出:对象未被回收
7.2 软引用(Soft Reference)
软引用是一种比较“温柔”的引用。它在内存不足时才会被 GC 回收。
SoftReference<Object> softRef = new SoftReference<>(new Object());
常用于内存敏感的缓存。
示例:
import java.lang.ref.SoftReference;public class SoftReferenceDemo {public static void main(String[] args) {Object obj = new Object();SoftReference<Object> softRef = new SoftReference<>(obj);obj = null;System.gc();if (softRef.get() != null) {System.out.println("软引用对象仍存活");} else {System.out.println("软引用对象被回收");}}
}
注意:此示例中的回收依赖内存状况,可能不会立即触发。
7.3 弱引用(Weak Reference)
弱引用在 GC 时总是会被回收。
WeakReference<Object> weakRef = new WeakReference<>(new Object());
特性:
-
非常适合使用在 ThreadLocal、元数据缓存等短生命周期场景。
示例:
import java.lang.ref.WeakReference;public class WeakReferenceDemo {public static void main(String[] args) {Object obj = new Object();WeakReference<Object> weakRef = new WeakReference<>(obj);obj = null;System.gc();if (weakRef.get() != null) {System.out.println("弱引用对象仍存活");} else {System.out.println("弱引用对象被回收");}}
}
输出:弱引用对象被回收
7.4 虚引用(Phantom Reference)
虚引用无法通过 get()
方法访问,被用于对象被回收时收到通知。
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), referenceQueue);
特点:
-
永远不会阻止 GC。
-
常与
ReferenceQueue
配合使用。
示例:
import java.lang.ref.PhantomReference;
import java.lang.ref.ReferenceQueue;public class PhantomReferenceDemo {public static void main(String[] args) {Object obj = new Object();ReferenceQueue<Object> queue = new ReferenceQueue<>();PhantomReference<Object> phantomRef = new PhantomReference<>(obj, queue);obj = null;System.gc();System.out.println("phantomRef.get(): " + phantomRef.get());System.out.println("是否进入 ReferenceQueue: " + (queue.poll() != null));}
}
输出:
phantomRef.get(): null
是否进入 ReferenceQueue: true
说明:虚引用不会返回实际对象,只用于跟踪对象是否已被 GC。
7.5 引用强度对比总结
引用类型 | 是否影响 GC 回收 | 典型用途 | 是否可通过 get() 访问对象 |
---|---|---|---|
强引用 | 否 | 普通对象引用 | 是 |
软引用 | 视内存情况而定 | 内存敏感缓存 | 是 |
弱引用 | 是 | ThreadLocal、临时元数据 | 是 |
虚引用 | 是(立即回收) | 清理前回调通知 | 否 |
8. 垃圾收集器与对象存活判定策略
Java 虚拟机中的垃圾收集器(GC)负责自动管理堆内存,及时回收不再使用的对象。不同的垃圾收集器采用不同的算法和策略来判断对象是否存活,从而决定是否回收。理解这些策略有助于优化程序性能和内存管理。
8.1 常见垃圾收集器简介
收集器名称 | 特点 | 适用场景 |
---|---|---|
Serial GC(串行收集器) | 单线程执行,简单高效 | 适合小内存或单核环境 |
Parallel GC(并行收集器) | 多线程并行,吞吐量优先 | 多核服务器环境 |
CMS GC(并发标记清理) | 低停顿,标记与清理并发执行 | 对响应时间敏感的应用 |
G1 GC(Garbage First) | 分区管理,低停顿,适合大堆 | 大内存多核服务器 |
8.2 对象存活判定的核心机制
无论使用哪种收集器,对象的存活判定都基于“可达性分析”(Reachability Analysis):
-
从 GC Roots(如线程栈、静态变量)开始,遍历所有引用链。
-
能被引用链访问到的对象被认为是存活的,不回收。
-
无法访问的对象则被标记为可回收。
8.3 不同收集器的对象判定流程
Serial 和 Parallel 收集器
-
标记-清除(Mark-Sweep)或标记-复制(Mark-Copy)算法。
-
先暂停应用(Stop-The-World),从 GC Roots 开始标记存活对象。
-
清除未标记对象或复制存活对象到新空间。
CMS 收集器
-
采用多阶段并发标记:
-
初始标记:暂停应用,标记直接可达对象。
-
并发标记:应用线程运行时,标记间接可达对象。
-
重新标记:短暂停止应用,完成标记遗漏部分。
-
并发清理:清理不可达对象。
-
G1 收集器
-
将堆划分成多个固定大小的区域(Region)。
-
并发标记阶段识别每个区域的存活对象数量。
-
优先回收存活对象少的 Region,减少停顿时间。
-
支持混合回收:回收年轻代和部分老年代。
8.4 代码示例:指定收集器启动参数
# 使用 Serial GC
java -XX:+UseSerialGC -Xmx512m -Xms512m MyApp# 使用 CMS GC
java -XX:+UseConcMarkSweepGC -Xmx2g -Xms2g MyApp# 使用 G1 GC
java -XX:+UseG1GC -Xmx4g -Xms4g MyApp
使用 VisualVM 或 JVisualVM 可以观察不同收集器下堆内存对象的存活情况。
9. 总结与实践建议
本文全面解析了 JVM 中对象存活判定的核心机制及其应用,包括可达性分析、引用类型、Finalize机制、垃圾收集器对判定策略的影响等关键内容。
9.1 对象存活判定的核心是“可达性分析”
-
通过从 GC Roots 出发遍历引用链,判断对象是否仍被程序访问。
-
只有不可达对象才有回收资格,确保安全且高效的内存管理。
9.2 多种引用类型助力内存优化
-
强引用、软引用、弱引用、虚引用各具特点,开发者可根据需求选择不同引用,灵活控制对象生命周期和内存回收时机。
-
理解它们的差异,有助于避免内存泄漏和提升程序稳定性。
9.3 Finalize机制存在风险,应尽量避免
-
finalize()
方法虽可让对象“复活”,但执行时机不确定,且影响性能。 -
推荐使用
java.lang.ref.Cleaner
替代,更加安全且高效。
9.4 不同垃圾收集器对对象存活判定实现有差异
-
串行、并行、CMS 和 G1 GC 等采用各自的标记算法和阶段,平衡吞吐量与延迟。
-
了解垃圾收集器特性,合理配置 GC 参数,对提升系统性能至关重要。
9.5 实践建议
-
在开发中,优先确保对象引用链清晰,避免意外的强引用导致内存泄漏。
-
结合软弱引用,设计缓存等场景,提高内存利用率。
-
监控和调优垃圾收集器,配合性能分析工具,及时发现和解决内存相关问题。
-
避免依赖
finalize()
,转用 Cleaner 和显式资源管理。 -
对于大型应用,考虑采用 G1 或者更先进的收集器,兼顾响应和吞吐。
通过深入理解对象存活判定方法,开发者能更精准地控制内存管理,写出高效、稳定的 Java 应用。