# 垃圾回收机制前置知识
作者:Ethan.Yang
博客:https://blog.ethanyang.cn (opens new window)
# 一、引用语义:谁在“指着”这个对象?
Java 通过引用强度影响对象的存活判定与回收时机。
# 1)强引用
只要存在强引用,GC 永远不会回收对象,即使内存紧张。
Object obj = new Object(); // 强引用
# 2)软引用
内存充足不回收,紧张时回收;常用于缓存。
SoftReference<Object> softRef = new SoftReference<>(new Object());
Object obj = softRef.get();
2
# 3)弱引用
发生 GC 就回收(不看内存是否紧张);常用于 WeakHashMap、ThreadLocal 的键等。
WeakReference<Object> weakRef = new WeakReference<>(new Object());
Object obj = weakRef.get();
2
# 4)虚引用
仅用于跟踪对象回收状态,不影响可达性。
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue);
2
这些引用不会直接改变 GC 算法,但会在 可达性分析 与 清理阶段 影响对象命运。
# 二、可达性分析:如何判断对象“还活着”
# 1)GC Roots 的来源
线程栈帧中的局部变量表引用
方法区(元空间)中的静态字段引用
运行时常量池中的引用
正在运行的线程对象本身
JNI(本地方法)引用
GC Roots 是堆外区域中的引用起点。
# 2)可达性分析算法
从 GC Roots 出发,沿着对象引用链递归查找:
标记阶段: 从 GC Roots 出发,把能沿引用链抵达的对象标记为“活”;
清理阶段: 没被标记的对象就是不可达对象,可以回收。
注意:可达 ≠ 正在使用;JVM 只以“从根是否可达”作为回收判定的客观标准。
# 三、对象标记机制与演进
# 1) 三色标记法
| 颜色 | 含义 |
|---|---|
| 白色 | 未被标记(可回收) |
| 灰色 | 已发现但其引用未扫描完 |
| 黑色 | 已扫描完且所有引用对象均可达 |
执行流程:
所有对象初始为白色;
GC Roots 标记为灰色;
不断扫描灰色对象,将其引用对象染灰,自身转为黑色;
扫描结束后,剩余白色对象为垃圾。
写屏障 & 记忆集(Card Table / RSet)
为解决并发标记时引用变更问题(如“黑指向白”),GC 利用写屏障和记忆集维护“三色不变式”:
A → B // A 黑、B 灰
A.field = C; // 若 C 白,则需重新标灰
2
如果C是白色, A->C 违反了三色定义, 黑色的对象是可达并且所有子引用都扫描完, C有引用不应该被回收, 但是按照三色标记法会被回收。不同年代的垃圾回收器有不同的处理方式。
# 2) 跨代引用与写屏障机制
由于对象在年轻代与老年代间存在跨代引用,GC 引入以下机制优化扫描效率:
写屏障(Write Barrier)
在引用写操作前后插入钩子,用于通知 GC “某个引用被修改了”。
write_barrier(A, B) { record_reference_change(A, B); A.field = B; }1
2
3
4Card Table(卡表)
堆被划分为固定大小的 Card 区块;
当老年代对象引用了年轻代对象,对应卡页被标记为脏;
Minor GC 时只扫描脏卡页,避免全堆扫描。
RSet(Remembered Set)
G1 收集器中每个 Region 自带一份“卡表”,记录其他 Region 指向本 Region 的引用; 避免全堆扫描,提高并行效率。
CSet(Collection Set)
G1 每次 GC 周期中选出的待回收 Region 集合:
- 包含 Eden、部分 Survivor、部分 Old Region;
- 基于“回收收益”模型优先清理垃圾比例高的 Region。
# 3) 收集器演进
早期阶段
代表收集器
Serial、ParNew、Parallel Scavenge
全程 Stop-The-World
垃圾回收时用户线程会暂停, 当完成可达性分析+回收工作后再恢复应用线程。
Minor GC 依赖卡表机制避免老年代全堆扫描, 解决跨代引用。
OldObj.field = YoungObj; // 写屏障示意 只要某个老年代区域引用了年轻代,就把对应的卡页打脏。 card_table[(address(OldObj) >> 9)] = DIRTY;1
2
3
并发标记阶段
代表收集器
CMS
标记阶段与用户线程并发
Minor GC 依赖卡表机制避免老年代全堆扫描, 解决跨代引用。
使用写后屏障防止“黑指向白”漏标问题;
// 问题 A → B (A 已标黑,B 已标灰) // 新增引用, 如果C为白色 A.field = C; // 解决方案 使用写屏障变更引用对象颜色 if (A is black && C is white) mark(C as gray) // 变更 mark bitmap 记录对象染色情况1
2
3
4
5
6
7
8
Region 化阶段
代表收集器
G1
处理方式
堆被划分为等大小 Region;使用 RSet 跟踪跨 Region 引用;通过 CSet 按收益优先选择回收目标。
# 四、核心回收算法
| 算法 | 过程 | 优点 | 缺点 | 典型应用 |
|---|---|---|---|---|
| 标记-清除 | 标记可达对象,清理未标记 | 实现简单 | 碎片多 | 老年代(基础实现) |
| 标记-复制 | 复制存活对象到新区域 | 无碎片,快 | 空间浪费 | 新生代 |
| 标记-整理 | 标记后移动对象 | 消除碎片 | 需移动对象 | 老年代(Compact GC) |
| 分代收集 | 按对象生命周期分代 | 效率高 | 实现复杂 | 各主流收集器 |
# 五、GC 触发条件
| 触发场景 | 类型 |
|---|---|
| Eden 或 Survivor 区满 | Minor GC |
| 老年代空间不足 / 晋升失败 | Major / Full GC |
元空间压力或 System.gc() 调用 | Full GC |
# 六、YGC 与 Full GC 的扫描边界
# 1)YGC(Minor GC / Young GC)
目标:只回收年轻代(Eden + Survivor)中的无用对象。
根对象来源(Roots):
- 线程栈、本地引用、静态字段
- 老年代中被标记为 Dirty 的卡页(Card Table / RSet)
回收流程: 从 Roots 开始扫描 → 标记年轻代存活对象 → 将其 复制 到 Survivor 区(或晋升到老年代)。
# 2)Full GC(Major GC)
- 触发:老年代不足、晋升失败或显式
System.gc()。 - 范围:全堆(新生代 + 老年代 + 元空间)。
- 并行收集器:全堆扫描;
- G1 / ZGC:基于 Region / Page 的精确扫描,依赖 RSet 或染色指针识别引用。