垃圾回收器
约 3493 字大约 12 分钟
2025-10-07
当一个对象被使用和引用时,称这个对象时存活的,当对象不再被使用或引用时,将会被 GC 检测并释放掉
工作方式
- 标记对象为存活状态:
- 遍历对象图来识别所有存活的对象
- 若对象能够被 GC 访问到,则视其为存活;若对象无法被 GC 访问,则视其为垃圾回收的候选对象
- 清理死亡状态的对象
- 将剩余存活的对象压缩整理,使其在内存中更紧凑
生代垃圾回收
为了避免高频率对对象的压缩整理,JVM 使用了生代垃圾回收机制,将对象按照「年龄」划分,一般而已存活越久的对象需要被回收的概率更低,垃圾清理的频率不需要那么高
JVM 中堆被划分为两个区域:新生代和老年代,以及堆外的永久代(JDK8 后,永久代被元空间所取代)
新生代
新生代的 GC 称为 Minor GC 划分为三个区域:
- Eden
- From(S0 存活区0)
- To(S1 存活区1) 对象创建时位于
Eden区,当进行一次Minor GC时,Eden与From中存活下的对象将会移动至To中,清空Eden与From中的对象,最后将To中的对象移动至From中 当对象在新生代区中移动的次数达到一定阈值时,将会被移动至老年代
老年代
老年代的 GC 被称为 Major GC
永久代/元空间
用于存放类与方法 JDK8 后,元空间取代了永久代。
- 永久代由 JVM 管理,受 JVM 的内存限制,容易发生内存不足错误
- 元空间位于直接内存,由操作系统管理,支持灵活扩容
GC 类型
当 GC 运行时,整个程序将会「暂停」以运行 GC,完成 GC 后恢复程序的运行 这一暂停的过程成为 Stop-The-World(STW) 垃圾回收器分为新生代和老年代的回收器,新生代 GC 与 老年代 GC 可以组合使用 
分代收集
Serial GC 串行 GC
所有的垃圾回收事件都在一个线程中串行进行,适用于小数据规模的数据集 暂停所有的用户线程,长时间的 STW
- 新生代->复制算法
- 将空间一分为二,每时每刻只有其中的一半用于保存数据,GC 的实现为将存活的对象移动至另一侧,同时实现了「整理」的效果

- 将空间一分为二,每时每刻只有其中的一半用于保存数据,GC 的实现为将存活的对象移动至另一侧,同时实现了「整理」的效果
- 老年代->标记-整理算法
- 将所有存活的对象移动至空间的一侧

- 将所有存活的对象移动至空间的一侧
Parallel New GC 并行 GC
Serial GC 的多线程版本,Server 模式下首选的新生代收集器,除了 Serial 外只有它能够与 [[#Concurrent Mark Sweep(CMS GC) 并发标记清除|CMS]] 配合工作 垃圾回收事件以多线程的方式进行,适用于多线程硬件下的中到大数据数据规模集 在对年轻代中的对象进行垃圾回收时就会使用多线程执行,而对于老年代则使用单线程执行
Concurrent Mark Sweep(CMS GC) 并发标记清除
用于老年代的处理
使用 标记-清除 算法 使用[[#三色标记法]]实现并行 与应用程序并发运行,以最小化 GC 暂停带来的影响 与之相对应的代价是会使用更高的 CPU,在非 CPU 瓶颈下是比并行 GC 更好的 GC 优点
- 停顿时间低,大部分耗时过程能够与用户进程并发 缺点
- 吞吐量低,低停顿时间的代价,CPU 利用率低
- 无法处理浮动垃圾,即在[[#清理阶段]]并发时用户进程产生的新垃圾无法在本轮 GC 中处理,需要留到下一轮 GC。为了平衡浮动垃圾,老年代必须降低 GC 的阈值,即预留空间用于浮动垃圾,如果预留的空间无法满足浮动垃圾与新对象,那么将会强制 STW 并使用 Parallel Old(Parallel GC 的老年代版本) 进行 full GC
- 标记-清除 算法会导致空间碎片的产生
分区收集
将堆划分为多个大小相等的独立区域(Region) 
Garbage First(G1 GC)
使用[[#三色标记法]]实现并行 Java 的默认 GC(JDK9 后),与 CMS GC 一样是并发与并行执行的 将堆划分为多个大小相等的区域(默认 2048 个),优先回收垃圾最多的区域
RSet(Remember Set)
每个 Region 使用 RSet 记录谁引用了我的对象,而 Card Table 表示我引用了谁的对象
上图中,每个 Region 被分为多个 Card,不同 Region 中的 Card 会相互引用。
作用
- 在 Young GC 时,使用 RSet 能够检查哪些对象被老年代中引用了,避免扫描所有老年代的开销
- 在 Mixed GC 时,要获取哪些对象引用了老年代的 Region 中的对象,只需要扫描其中的 RSet 便能知道哪些年轻代或老年代引用了自己
GC 过程
G1 提供两种 GC 模式:Young GC 与 Mixed GC G1 会维护 Eden Region 列表和空闲 Region 列表 维护 CSet,集 GC 的集合,用于记录将要回收的对象
Young GC
触发条件:当所有的 Eden Region 已满且无空闲的 Region 时 会触发短暂的 STWCSet:年轻代(Eden、Survivor) 从 GC Root 出发,进行扩散扫描,检查对象是否可达(只有年轻代的对象才会被加入);检查年轻代中每个 Region 中的 [[#RSet(Remember Set)|RSet]] 检查是否被老年代中的对象所引用 使用复制算法清理无用对象,将存活对象复制至新的 Region
Mixed GC
触发条件:当堆的使用率达到-XX:InitiatingHeapOccupancyPercent(默认为 45%) 时触发 [[#并发标记]];当并发标记完成后的下一次 [[#Young GC]] 将升级为 Mixed GCCSet:年轻代(Eden、Survivor)和回收率较高的老年代 从触发条件中不难发现,Mixed GC 需要经过两个阶段:
- 并发标记
- 复制算法清理 此外, Young GC 相对来说是高频的,且 G1 的目的就是尽可能的降低停顿时间,反正更高频的 Young GC 每次都要 STW,那么为什么不考虑在这段 STW 的过程中多干点事呢?
- 条件:达到堆使用率的阈值
- 下一次 Young GC 到达,Young GC 触发 STW 并从 GC Root 开始进行扫描,在这过程中,可以使用 [[#SATB]] 与 [[#三色标记法]] 对老年代中的 region 统计存活率
- 此时本次的 Young GC 结束了,CSet 中也存下了老年代中需要清理的 region
- 下一次 Young GC 到来了,将其拦截并升级为 Mixed GC。在 Mixed GC 中,依旧通过 [[#Young GC]] 中的计算方法得到新生代中要回收的 region
- 此时的 CSet 包含了所有年轻代和部分老年代中需要清理的目标,因为 CSet 相较于普通的 Young GC 更大,因此本次的 GC 耗时更长。Mixed GC 也可以理解为时间更长的 Young GC
分析
G1 的两种 GC 都不是 Full GC,Mixed GC 只能回收部分老年代的 Region,若 Mixed GC 无法跟上程序分配内存的速度,导致老年代填满,无法分配新对象,会触发 Full GC(Serial GC 或 Parallel GC)
Epsilon GC
发布于 JDK11 只执行内存的分配而不做内存的回收,一旦堆空间溢出,JVM 将会直接停止 适用于对延迟极其敏感的应用程序,且开发者明确了解内存的使用
Shenandoah GC
使用[[#三色标记法]]实现并行 发布于 JDK12 与 G1 类似,对堆划分区域 使用并发压缩实现在用户线程运行时 GC 依旧能够安全的移动对象,极大降低了 STW 的时间,尤其是大堆下 在移动对象前,先将对象复制至新区域,然后原子的将原对象的Brooks Pointer字段执行新地址。代价是每个对象需要多 8 字节(64位指针)
ZGC
使用[[#三色标记法]]实现并行 发布于 JDK11 ,改进于 JDK12 基于并发标记+并发重定位+并发引用 使用多重内存映射实现并发引用更新,程序中存储的是对象的虚拟的值,JVM 同过更改虚拟地址映射的真实物理地址实现并发引用,相较于Shenandoah GC能够进一步降低STW 适用于需要低延迟或大堆的程序,
三色标记法
将对象的颜色分为了黑、灰、白 三种颜色
- 白色:该对象没有被标记过
- 灰色:该对象已经被标记过了,但对象下的属性没有被全部标记完
- 黑色:该对象已经标记过了,且该对象下的属性也已经全部被标记过 -> 不是垃圾

算法
所有对象默认为白色
初始标记
需要 STW 从 GC Root 出发,将直接可达的对象标记为灰色。为了防止根扫描的过程中根被修改,因此根扫描过程中会触发 STW
并发标记
GC 线程不断从灰色对象集合中取出对象
- 扫描它引用的所有子对象
- 若子对象为白色的,将子对象标记为灰色
- 当所有的子对象都处理完后,将该对象标记为黑色 重复以上流程直至灰色集合为空
重新标记
需要 STW 目的:因为并发标记的实现,存在先前认为「不可达」的对象被引用,此时那个对象不应该被清理 实现:通过写屏障将并发标记过程中引用了其他对象的对象所在的内存区域标记为脏,即意味着这些区域可能发生了「黑色对象引用了白色对象」,通过 STW 保证那些被引用的白色对象不会被误删除
清理阶段
将所有仍是白色的对象标记为垃圾 -> 回收
遗漏标记
使用「写屏障」实现,与 AOP 相似,在引用被覆盖或删除前触发
场景
当 GC 线程与用户线程并行时,若一个对象 A 被 GC 标记为黑色后,用户线程为对象 A 新建了一个指向对象 B,且这个 B 对象仍为白色。此时就会出现 B 存在被 A 的引用,却被 GC 当做垃圾(白色)回收了
解决
JVM 的并发标记过程中使用位图的标记方式
并发标记的过程中,GC 将对大部分存活的对象进行标记 对于 GC 过程中的对象引用关系的变化,需要使用屏障技术
- CMS 使用增量更新
- 一旦发现黑色对象重新引用了白色对象,则将黑色对象退回至灰色
- 当出现以上情况时会出发写屏障,GC会将其重新标灰
- 当出现黑色对象退回灰色的情况后,重新标记阶段需要从 GC Root 开始重新扫描,效率相对较低
- 依旧存在漏标问题:
- 对于一个标为灰色的对象 A,GC 正在遍历其子对象,而在此时它引用了一个白色的对象 B,A 依旧为灰色。但 GC 线程标记完其所有的子属性后(不包含 B),将 A 标记为黑色,B 就被漏标了
- G1 使用原始快照 [[#SATB]](Snapshot At The Beginning)
- 通过只清理一定需要清理的对象的策略能够使扫描的效率大于 CMS,属于使用空间换时间
SATB
SATB 抽象指的是再一次 GC 开始时,活的对象就是活的;新分配的对象就是活的;其余不可达的对象是死的
- 对于在快照中的对象,如果在扫描过程中被标记为活的(黑色),那么无论在 GC 过程中这个对象的引用是否被断开,始终认为其是活的。(如果它该死,那么留到下次 GC 再死)
- 对于新分配的对象,即便他们的生命周期非常短暂,朝生夕死,在这轮 GC 中也认为他们是活的,是否清理留到下次 GC 再判断
- 对于存在于快照中,没有任何引用的对象,将始终不可达,应当被清理 写屏障逻辑
void write_barrier(Obj* field, Obj* new_value) {
Obj* old_value = *field;
if (old_value != null &&
old_value.color == WHITE &&
old_value.in_snapshot) { // 是否在快照中
SATB_Buffer.add(old_value); // 加入待处理队列
}
*field = new_value;
}logging write barrier
为了尽量减少写屏障对的 mutator(变更器,指的是应用程序线程) 的影响,写屏障只是将要做的事情记录到一个队列中,后续从队列中取出消息统一处理
- SATB 中,每一个 Java 线程有一个独立的、定长的 SATBMarkQueue,用于存储引用变更后旧值的地址
- 当线程的队列满或完成并发标记后,会将线程的队列中的内容移动至全局的 SATB 队列中等待处理
- 并发标记(concurrent marker)线程会定期检查全局 SATB 队列,当全局集合数量超过一定阈值后,遍历集合中的 OOP(ordinary object pointer 对象指针),若该对象是白色且在 SATB 快照中
- 标记为灰色
- 将字段压入标记栈,等待后续传播标记
