1. 为什么要了解垃圾收集?
虽然 Java 的内存动态分配和回收看起来是自动化的,但在以下场景中,我们需要深入了解其原理以进行必要的监控和调节:
-
排查故障: 当出现内存溢出(OOM)、内存泄漏时。
-
性能调优: 当 GC 成为系统达到更高并发量的瓶颈时。
内存回收的"主战场"
Java 运行时区域分为两类,GC 主要关注的是动态分配的区域:
| 区域 | 特性 | 是否需要关注 GC | 原因 |
|---|---|---|---|
| 程序计数器、虚拟机栈、本地方法栈 | 随线程生灭 | ❌ 不需要 | 栈帧出栈或线程结束时内存自然回收,具备确定性。 |
| Java 堆、方法区 | 动态分配 | ✅ 重点关注 | 只有在运行时才知道会创建哪些对象、多少对象。 |
2. 判断对象是否存活的算法
在对堆进行回收前,必须确定哪些对象还"活着",哪些已经"死去"。
A. 引用计数算法 (Reference Counting)
-
原理: 对象添加计数器,被引用+1,引用失效-1,为0则可回收。
-
优点: 简单、效率高。
-
缺点: 难以解决对象间循环引用的问题(如 A 引用 B,B 引用 A,除此之外无其他引用,导致无法回收)。
-
现状: 主流 Java 虚拟机未采用此算法。
B. 可达性分析算法 (Reachability Analysis)
这是主流商用语言(Java, C#)采用的算法。
-
原理: 从一组称为 "GC Roots" 的根对象出发,向下搜索。如果一个对象到 GC Roots 没有任何引用链 (Reference Chain) 相连,则证明该对象不可用。
-
常见的 GC Roots 对象:
-
虚拟机栈(局部变量表)中引用的对象。
-
方法区中类静态属性引用的对象。
-
方法区中常量引用的对象(如字符串常量池)。
-
本地方法栈中 JNI(Native 方法)引用的对象。
-
Java 虚拟机内部引用(如基本数据类型对应的 Class 对象、异常对象)。
-
被同步锁 (
synchronized) 持有的对象。
-
3. Java 的四种引用强度 (JDK 1.2+)
判定对象存活与"引用"密切相关。Java 将引用分为四种,强度由高到低:
| 引用类型 | 描述 | 回收时机 | 用途示例 |
|---|---|---|---|
| 强引用 (Strong) | 最普遍的引用 (如 new Object())。 |
永不回收(只要引用关系还在)。 | 普通对象实例化。 |
| 软引用 (Soft) | 还有用但非必须的对象。 | 内存溢出前进行二次回收。 | 缓存功能。 |
| 弱引用 (Weak) | 非必须对象,强度比软引用更弱。 | 下一次 GC 时无论内存是否足够必回收。 | WeakHashMap,缓存。 |
| 虚引用 (Phantom) | 最弱,无法通过它获取对象实例。 | 无影响,回收时收到系统通知。 | 跟踪对象回收状态,资源清理。 |
4. 对象死亡的"缓刑"与自救
即使在可达性分析中不可达,对象也不一定会立即死亡,它有一次自我拯救的机会。
-
第一次标记: 对象不可达,进行筛选。如果对象覆盖了
finalize()方法且未被执行过,则放入F-Queue队列。 -
执行 finalize(): 虚拟机会触发该方法(但不保证等待其运行结束)。
-
自我拯救: 在
finalize()中将自己(this)赋值给某个类变量或成员变量,即可逃脱回收。 -
第二次标记: 如果没逃脱,则被回收。
⚠️ 重要提示:
finalize()运行代价高、不确定性大且只能执行一次。官方明确不推荐使用 ,建议使用try-finally块替代资源关闭工作。
5. 方法区的回收
方法区(如永久代或元空间)的回收条件非常苛刻,主要回收两部分:
-
废弃的常量: 如没有任何对象引用的字符串常量。
-
不再使用的类型 (Class): 必须同时满足以下三个条件:
-
该类所有实例都已被回收。
-
加载该类的 ClassLoader 已被回收。
-
该类对应的
java.lang.Class对象没有在任何地方被引用(无法通过反射访问)。
-
这种回收常见于大量使用反射、动态代理、OSGi 等频繁自定义类加载器的场景。
核心知识点问答
1. 问题:为什么 Java 内存管理已经自动化了,我们还需要了解垃圾收集(GC)?
解答: 虽然自动化了,但在以下情况我们需要实施监控和调节:
-
需要排查各种内存溢出(OOM)、内存泄漏问题时。
-
当垃圾收集成为系统达到更高并发量的性能瓶颈时。
2. 问题:Java 运行时内存区域中,哪些是垃圾收集器关注的重点?为什么?
解答: GC 主要关注 Java 堆(Java Heap)和方法区(Method Area)。
- 原因 :程序计数器、虚拟机栈、本地方法栈这三个区域随线程而生灭,内存分配和回收具备确定性(栈帧大小编译期基本可知),方法结束或线程结束时内存自然回收。而 Java 堆和方法区的内存分配在运行期间是具有显著不确定性的(例如不同条件分支创建的对象数量不同),GC 关注的正是这部分动态内存的管理。
3. 问题:判断对象是否存活的"引用计数算法"有什么优缺点?为什么主流 Java 虚拟机不使用它? 解答:
-
原理:在对象中添加引用计数器,引用加一,失效减一,计数为零则不可用。
-
优点:原理简单,判定效率高。
-
缺点(核心原因) :很难解决对象之间相互循环引用的问题。如果两个对象互相引用但不再被其他地方引用,它们的计数都不为零,导致无法回收。
4. 问题:主流商用程序语言(如 Java)目前使用什么算法判定对象是否存活?基本思路是什么? 解答: 使用的是可达性分析算法(Reachability Analysis)。
- 基本思路 :通过一系列称为"GC Roots"的根对象作为起始节点集,从这些节点开始根据引用关系向下搜索,走过的路径称为"引用链"。如果某个对象到 GC Roots 间没有任何引用链相连(即不可达),则证明此对象不再被使用。
5. 问题:在 Java 中,哪些对象可以固定作为 GC Roots?
解答:
-
虚拟机栈(栈帧中的局部变量表)中引用的对象。
-
方法区中类静态属性引用的对象。
-
方法区中常量引用的对象(如字符串常量池)。
-
本地方法栈中 JNI(Native 方法)引用的对象。
-
Java 虚拟机内部的引用(如基本类型的 Class 对象、常驻异常对象、系统类加载器)。
-
所有被同步锁(synchronized)持有的对象。
-
反映 JVM 内部情况的 JMXBean 等引用的对象。
6. 问题:JDK 1.2 之后,Java 对"引用"的概念进行了哪些扩充?它们的区别是什么?
解答: Java 将引用分为 4 种强度(由强到弱):
-
强引用 (Strong Reference) :最传统的引用(如
new Object()),只要存在,GC 永远不会回收。 -
软引用 (Soft Reference):描述还有用但非必须的对象。在系统将要发生内存溢出异常前,会列入回收范围进行二次回收。用于缓存。
-
弱引用 (Weak Reference):非必须对象,只能生存到下一次 GC 发生之前。无论内存是否足够都会被回收。
-
虚引用 (Phantom Reference):最弱的引用。完全不影响对象生存时间,无法通过它获取对象实例。唯一目的是在对象被回收时收到系统通知。
7. 问题:一个对象在可达性分析中被判定不可达后,就立即死亡了吗?它还有机会自救吗?
解答: 不是立即死亡,处于"缓刑"阶段。要宣告死亡至少经历两次标记过程。 对象有机会自救,但只有一次机会:
-
如果对象覆盖了
finalize()方法且尚未被执行,它会被放入 F-Queue 队列等待执行。 -
在
finalize()方法中,如果对象重新与引用链上的任意对象建立了关联(譬如把自己赋值给某个类变量),它在第二次标记时就会被移出"即将回收"的集合,实现自救。 -
注意 :任何一个对象的
finalize()方法最多只会被系统自动调用一次。官方不推荐使用该方法。
8. 问题:方法区(如元空间)需要垃圾收集吗?回收的条件是什么?
解答: 方法区有垃圾收集行为,但性价比通常较低。主要回收两部分内容:
-
废弃的常量:没有对象引用该常量且虚拟机中无其他地方引用。
-
不再使用的类型(类):判定条件非常苛刻,需同时满足三点:
-
该类的所有实例都已经被回收。
-
加载该类的类加载器已经被回收。
-
该类对应的
java.lang.Class对象没有在任何地方被引用(无法通过反射访问)。
-
finalize() 方法
是 Java 中一个特殊的方法,它是在对象被垃圾收集器回收之前,由虚拟机自动调用的一个方法。
以下是关于 finalize() 方法的一些关键点:
-
拯救对象的机会 :它是对象逃脱死亡命运的最后一次机会。如果一个对象在可达性分析中被判定为不可达,它会被第一次标记。如果该对象覆盖了
finalize()方法且该方法尚未被虚拟机调用过,那么这个对象会被放入一个 F-Queue 队列中。稍后,虚拟机会建立一个低优先级的 Finalizer 线程去触发这个方法。 -
自我拯救 :在
finalize()方法中,对象可以通过重新与引用链上的任何一个对象建立关联(例如把this关键字赋值给某个类变量或对象的成员变量)来拯救自己。如果拯救成功,在第二次标记时它将被移出"即将回收"的集合。 -
只调用一次 :任何一个对象的
finalize()方法都只会被系统自动调用一次。如果对象在finalize()中拯救了自己,但之后再次面临回收,它的finalize()方法将不会被再次执行。 -
不确定性 :虚拟机虽然会触发
finalize()方法的执行,但并不承诺会等待它运行结束。这是为了防止某个对象的finalize()方法执行缓慢或发生死循环,从而导致 F-Queue 队列阻塞,甚至拖垮整个内存回收子系统。 -
不推荐使用 :
finalize()方法的运行代价高昂,不确定性大,且无法保证各个对象的调用顺序。它已被官方明确声明为不推荐使用的语法。建议开发者尽量避免使用它,可以使用try-finally或其他方式来替代它进行资源清理工作。
