三色标记法本质是将全量可达性分析拆为可中断的微任务,用白(未访问)、灰(待处理)、黑(已扫描)逻辑状态维持一致性,并依赖写屏障捕获并发引用变更,实现低延迟增量标记。

三色标记法在 V8 的增量标记阶段,本质是把一次长停顿的“全量可达性分析”拆成多个微小、可中断、可恢复的扫描单元,靠颜色状态维持中间一致性,并借助写屏障捕获并发修改,从而实现几乎无感的内存回收。
三色状态不是真实染色,而是逻辑划分
白色、灰色、黑色不对应内存中的实际字节,而是 GC 线程维护的三个逻辑集合(或对象状态位):
- 白色:尚未被 GC 访问过,默认状态;若标记结束仍为白色,且无法从 GC Roots 到达,则判定为垃圾
- 灰色:已入队待处理,其自身被访问过,但所引用的对象还未全部检查;它是“工作队列”的入口点
- 黑色:已完全扫描完毕,自身及其所有直接/间接引用对象都确认存活;黑色对象不再参与本轮标记推进
增量标记如何“分步穿插”执行
V8 不等应用暂停才启动标记,而是在 JavaScript 引擎空闲间隙(如事件循环空转、帧渲染间隔)主动插入一小段标记任务:
- 每次只从灰色集合中取出几十个对象,遍历其引用,把新发现的白色对象涂灰、自身涂黑
- 执行完当前小批次后立即交还控制权给 JS 主线程,下次空闲时从中断处继续
- 整个灰色集合像一个“待办清单”,逐步清空;当它彻底为空,且无写屏障记录的脏引用,标记即完成
写屏障解决并发漏标问题
用户线程运行时可能修改引用关系,导致本该存活的对象被跳过(漏标)。V8 在赋值操作(如 a.b = c)前后插入写屏障逻辑:
- 若
c是白色对象,而a已是黑色,这次赋值会把c强制重新标灰(或加入灰色队列) - 这相当于告诉 GC:“这个白色对象刚被黑色对象‘拉进来’了,必须重查”
- V8 使用的是 增量更新(Incremental Update) 类型写屏障,侧重拦截新增引用
为什么能避免长时间 STW
初始标记阶段仍需短暂 STW(仅扫描 GC Roots 直接引用),但后续所有标记工作都在 JS 运行中渐进完成:
- 单次标记耗时控制在毫秒级(通常 ≤ 5ms),远低于人眼可感知卡顿阈值(16ms)
- 标记进度持续累积,无需等待堆中所有对象扫完才进入清除;清除可与后续标记并行
- 即使 JS 突然高负载,GC 也会自动降频或暂停增量步骤,优先保障业务响应
不复杂但容易忽略:三色标记本身不保证绝对零停顿,它的价值在于把不可控的大停顿,转化成可控、可预测、对用户体验透明的小节奏——这才是现代 JS 引擎流畅运行的底层支点。

















