JVM 三色标记中:增量更新与原始快照
下面用最简单、最容易理解的例子把增量更新和原始快照讲清楚。你可以把这两个机制想象成:
增量更新:处理 “新加的引用”
原始快照:处理 “被删除的引用”
🟦 背景(必须知道)
三色标记法:
白色:未标记,可能是垃圾
灰色:被发现但子对象未扫描
黑色:扫描完成
并发标记时,程序还在运行,会随时修改对象引用。这些引用修改会破坏三色不变式,所以需要 写屏障(Write Barrier) 记录这些变化,等标记结束后“补扫”(Remark)。
🟩 第一部分:增量更新(Incremental Update)
破坏的条件:黑色对象不能指向白色对象
作用:处理 “新加的引用”
🎯 重点一句话
如果一个黑色对象新增引用一个白色对象,就把这个黑色对象重新变成灰色,留待重新扫描。
🧩 简单例子
假设有对象:
A -> B -> C
开始并发标记后:
A 已扫 → 黑
B 已扫 → 黑
C 尚未扫描 → 白
此时应用线程做了一个动作:
B(黑).next = C(白)
这就违反了三色规则:黑不能直接指向白。
所以增量更新的写屏障会记录:
记录:B 新增引用 C
并且把 B 重新标为灰色:
B: 灰色
标记结束前,会重新从 B 开始扫描,把 C 加入存活。
📝 结果
C 不会被错误地回收,因为 B 被“拉回来”扫描了一次。
🟥 第二部分:原始快照(Snapshot-at-the-Beginning,SATB)
破坏的条件:灰色对象指向白色对象的引用不能在扫描前被删除
作用:处理 “被删除的引用”
🎯 重点一句话
从并发扫描开始那一刻起,那些原来存在的引用,就算后来被删掉,也要按“快照”继续把它当作存在来处理。
🧩 简单例子
对象关系最开始为:
A -> B -> C
标记开始时,此图被当作快照。
并发扫描时:
A 扫完 → 黑
B 正在灰色队列(灰)
C 还未扫描(白)
这时应用线程删除了引用:
B(灰).next = null // 删除指向 C 的引用
按三色规则:
如果 B(灰色)删掉指向 C(白色)的引用,C 就可能被“漏标”。
所以 SATB 执行写屏障:
记录:B 删除了对白色对象 C 的引用
等扫描结束时,会补扫:
从 B(记录的对象)重新扫描,把 C 当做存活处理
📝 结果
C 仍然不会被误回收,因为它是快照开始时存活的对象。
🟧 把两者放在一起理解(最重要)
🟩 最通俗的比喻
增量更新 = “增加的人必须补登记”
你在做学生名单点名(并发标记),
某个“已经点过名的学生(黑)”忽然说:
“老师,我刚带来一个新同学(白)。”
你就必须:
把这个学生重新加入点名队列(变灰)
再把新来的同学补点名一次
原始快照 = “名单上有的人就算离开教室也算来过”
点名开始时名单上有 A → B → C
中途 B 说:“C 走了,我不认识他了(删除引用)。”
但老师说:
开始点名时你明明认识他,所以我还是要把他当成来过。
于是把 C 加入点名(存活)。