G1并发标记采用基于工作单元划分、线程协作与SATB写屏障驱动的增量式机制,不依赖传统调度算法。它将标记拆为可中断工作单元,由多线程窃取执行;通过SATB捕获旧引用覆盖以避免漏标,接受新增引用导致的多标;动态协同应用线程,控制吞吐量并响应延迟敏感场景;最终通过预清理合并日志、再处理对象,压缩Final Mark STW时间。

G1 的并发标记任务不依赖传统“调度算法”(如时间片轮转或优先级队列式调度),而是采用基于工作单元划分、线程协作与写屏障驱动的增量式执行机制。它的核心不是抢占式调度,而是让标记任务自然融入应用运行节奏,在可控开销下完成全堆可达性分析。
并发标记如何启动和分段执行
并发标记不会一次性扫描整个堆,而是被拆解为多个可中断、可恢复的工作单元(work packets),每个单元对应一部分对象图遍历任务。这些单元由一个全局任务队列管理,多个 GC 线程从队列中窃取(work-stealing)任务并执行:
- 初始标记(STW)完成后,G1 立即启动并发标记线程,但仅在应用负载较低时才真正开始大量工作;
- 标记过程被划分为多个“标记周期”(marking cycles),每个周期处理一部分 Region 和其引用关系;
- 每个 Region 的标记进度被记录在 bitmap 中,支持断点续标,避免重复扫描。
SATB 写屏障驱动的变更捕获
并发标记期间,Java 线程持续修改对象图(如赋值、新建引用)。G1 使用 SATB(Snapshot-At-The-Beginning)写屏障来捕获“旧引用被覆盖”的动作,确保标记结果不漏标:
- 当某字段从指向 A 改为指向 B 时,写屏障会将原引用 A 记录进 SATB 日志缓冲区;
- 这些日志被异步合并到全局标记栈,作为灰色对象重新扫描的输入;
- 新增引用(如 A→C)不被捕获,会导致“多标”(浮动垃圾),这是 G1 主动接受的设计权衡,以换取更短的 STW 和更低的写屏障开销。
与应用线程的协同节奏控制
G1 不强制抢占 CPU,而是通过以下方式与用户线程共存:
- 标记线程主动检查应用线程的 safepoint 请求,必要时让出 CPU;
- 使用自适应的“并发标记吞吐量阈值”(默认约 25%),限制 GC 线程占用应用线程的时间比例;
- 当系统检测到应用延迟敏感(如响应时间突增),可动态降低并发标记线程数或暂停部分任务。
最终标记前的日志合并与再处理
并发标记结束后,并非直接进入 Final Mark,而是先做一次“预清理”(Preclean):
- 批量消费 SATB 日志,将其中记录的对象推入标记栈;
- 对已标记为存活、但其引用关系可能变化的对象(如某些老年代对象)进行二次扫描;
- 这一步尽量减少 Final Mark 阶段的 STW 时间,使最终停顿更可控、更短。

















