资讯动态

06-02-源码-增量标记算法实现

发布时间:2026/10/22 7:29:05 来源:尧图企业网站定制
源码深度增量标记算法实现篇章06-源码深度-Unity增量式GC |阅读时间约 50 分钟 |前置知识增量标记原理、对象图遍历一、引言与前提纠正本章要回答的问题只有一个Unity 增量 GC 的标记阶段在源码层面到底是怎么实现的。它是怎么把标记整个堆这件几十毫秒的大事切成小步的切完之后状态存在哪里标记过程中世界还在变又靠什么兜住正确性。SGen 从来没有随 Unity 产品发布过Unity 的增量 GC 就是 Boehm-Demers-Weiser 收集器的 incremental modeMono 与 IL2CPP 两个后端皆是如此03-04 章已经划清了这条界线。本文将那版大纲全部作废如果你在别处看到把 Unity 增量 GC 和 SATB、三色屏障绑在一起当Unity 源码讲的文章默认那是对其他收集器的移植想象。错误模型的危害不止名字不对它会让你找错调优对象。按三色 SATB的模型你会去关心屏障队列长度、灰栈水位这类在 Unity 里根本不存在的东西。真实的 Boehm 增量标记正确性基石不是写时快照而是页粒度的脏页重扫推进引擎不是每帧弹 N 个灰对象而是按工作亏空记账的GC_mark_some。本章接下来八节就沿着这两个真实机制展开先回顾 Boehm 的标记算法本身第二节再看它的增量推进器第三节和大数组断点续扫第四节然后是正确性的核心脏页复查第五节、增量窗口的 STW 边界第六节、残余风险第七节最后与其他收集器做算法层对比第八节。调度层面每帧给多少时间、切不完怎么办的细节属于 06-03 章写屏障的生成细节属于 06-04 章本章只讲标记算法内部。二、Boehm 标记算法回顾2.1 标记栈与灰色对象推进Boehm 是标准的标记-清扫收集器标记阶段的核心数据结构是一条标记栈mark stack。所有从根或已标对象里发现的已标记但子引用未扫描的对象压入栈中标记器不断弹栈、扫描其子引用、把新发现的未标记对象再压回去直到栈空。示意非源码逐行对应 根集合栈槽、静态区、寄存器、注册根 │ 逐个读出可疑值先做保守识别 ▼ 标记栈[ objA, objB, ... ] ← 栈里的对象是待扫描状态 │ pop ▼ 扫描 objA 的每个引用槽 ├─ 槽值指向未标记对象 → 打标记、压栈 ├─ 槽值指向已标记对象 → 跳过 └─ 槽值不像堆内指针 → 忽略保守扫描的日常 ▼ 回到循环开头直到栈空 → 标记完成这里没有经典教科书里的灰→黑显式染色切换。Boehm 的标记信息压在对象的标记位上灰这个状态由标记栈本身承载在栈里就是待扫描的弹出来扫完就是黑了只是没有一个单独的黑色字段记录这件事。理解这一点很重要因为后面讲增量时你会发现增量要保存的断点就是这条栈的位置和内容而不是一套颜色位图的状态机。标记栈本身也有工程细节。栈空间是有限的Boehm 的处理方式是栈满时把一部分对象降级记到溢出结构里甚至临时退回保守策略处理局部块。对增量模式而言这意味着标记状态不止一条栈还包括与之配套的溢出记录与当前扫描游标这些全部位于 GC 管理的持久内存区域不属于任何一帧调用的栈帧这是后面第三节跨帧续扫成立的物理基础。2.2 GCJ 风格的描述符标记Boehm 支持一个在同类收集器里相当特殊的模式GCJ 风格的元数据标记。它最初为 Java 运行时设计做法是把某个对象内部哪些位置是指针的描述信息以函数指针描述符的形式挂在类型元数据上标记器扫到这类对象时调用对应的描述函数由它告诉标记器哪些字节范围内是指针、偏移如何跳转。对 Unity 这条链路来说IL2CPP 生成的代码并不走 GCJ 描述符这条路径它用的是更常规的按布局扫描。但这个机制对理解第四节是必要的铺垫因为 Boehm 处理值类型大数组的增量扫描入口恰恰挂在 GCJ 向量扫描的机制上。你只需要记住它的形状对象的扫描方式不是一刀切地把整块内存当指针数组读而是按类型走不同的扫描函数每种扫描函数都可能有自己的扫描到一半的进度状态。2.3 精确扫描与保守扫描两条路径Boehm 在引用识别上是保守的任何字里存的值只要落在某个已分配堆块的地址范围内就被当成引用处理不管它在托管语义里是不是真的指针。这条策略贯穿始终包括增量模式。但在扫描执行层面它其实有两条并存的路径按布局扫描编译器或绑定层知道对象的指针布局比如 IL2CPP 侧知道一个托管对象哪些字段是引用扫描函数只读指针槽跳过纯值字段。这条路更接近精确 GC 的执行效率但识别策略仍是槽里的值落在堆内就当引用所以仍是保守判定加精确布局的混合体整块保守扫描对布局未知的内存比如GC_MALLOC_UNCOLLECTABLE分配的裸缓冲、部分内部结构逐字读取每个字都做指针识别。值类型数组是一个特例它内部很多槽根本没有指针比如int[]、float[]但 Boehm 无法从类型系统层面确定这一点时GCJ 模式下描述符标记为向量类型、或者绑定层没有提供布局信息时会走向量扫描路径把整个数组内存当连续区域快速过一遍做保守识别。一个几万元素的值类型数组整块扫完可能就吃掉几毫秒这直接和每帧只给 GC 一个小时间片的增量目标冲突。第四节会看到 Boehm 怎么解决它。三、增量化的本质GC_mark_some 是可分步的标记推进器3.1 一次调用只推进有限工作把第二节的标记器想象成一个函数GC_mark_some它的任务是推进标记直到完成但被改造成每次调用只干一段有限的活然后返回。返回后标记栈、溢出结构、扫描游标原封不动地留在 GC 的持久内存里等下一次调用从原地续起。这就是增量标记的全部魔法所在没有别的秘密。推进器的上游是alloc.c里的GC_collect_a_little_inner(int n)它是引擎侧每次泵一小步最终落到的函数。它的核心是一个按工作亏空work deficit推进的循环示意如下// 示意GC_collect_a_little_inner 的亏空循环结构 // 对照 Boehm alloc.c省略错误处理与统计细节 void GC_collect_a_little_inner(int n) { int i 0; int deficit GC_rate * n; // 本步预算工作量单位不是毫秒 while (i deficit GC_mark_some((ptr_t)0) 0) { // GC_mark_some 返回 0 标记还没完继续推进 i; } // 若标记已完成且时间还有余GC_stopped_mark(GC_timeout_stop_func) 收尾让路 // 若连续多次切片仍完不成GC_n_attempts 耗尽 // GC_stopped_mark(GC_never_stop_func) GC_finish_collection() // 回退为一次做完调度细节见 06-03 }注意预算单位。GC_rate * n记的是工作量亏空不是毫秒数。每完成一小步标记就扣减一份亏空扣够了就返回时间上限只是外层防止干过头的保险丝。这回答了一个常见疑问增量标记为什么能精确地断点续扫因为它压根不是按时间切完再存状态而是每一步都建立在一个本来就持久的状态机上时间片到点中断时状态天然合法。3.2 任意一次返回后堆都处于合法中间态合法中间态值得展开说。GC_mark_some返回的那一刻系统满足三个不变式标记栈里的每个对象都已打上标记位只是子引用还没扫完已扫完的对象其所有子引用要么已标记要么被判定为非引用还没有任何白色对象被回收清扫尚未开始。这三条合起来意味着此刻停止标记、直接进入清扫也不会把根本可达但还没发现的对象之外的东西误伤但会把本该存活却还没来得及发现的对象当垃圾清掉也就是漏标。所以 Boehm 不能随便在中间态转入清扫必须把标记做完、或把中间态的欠账脏页在收尾时补齐。反过来中间态允许被打断无论在哪一步返回下一次调用都从当前栈顶和游标继续不需要重头再来也不需要重建任何上下文。这就是可重入、可断点续扫的准确含义。增量标记的一轮时间线 帧 k 帧 k1 帧 k2 ... 帧 km [推进一步] [推进一步] [推进一步] [推进一步] │ │ │ │ └─────────┴─────────┴─── 状态常驻于 GC 持久内存 ──┘ │ ▼ 标记完成 → 收尾 STW → 清扫增量窗口期间应用线程照常跑、照常改引用第五节处理由此产生的问题。这里先把推进器本身的行为说完整。3.3 推进器视角下的两种返回从调用者视角一次推进的结果只有两种这一步干完了但整轮标记还没完GC_mark_some返回未完成循环继续或本次时间片到点返回或者标记已经收尾。收尾路径有两种形态如果时间预算还有剩余用可超时的停止函数做一次GC_stopped_mark把剩余标记一口气做完并让出如果连续多个切片都完不成改引用的速度追上了标记速度GC_n_attempts一族计数器耗尽就用永不停止的停止函数强行做完整轮外加GC_finish_collection()收摊。后者就是 03-04 章讲的回退06-03 章会展开它的调度账本。本章只强调边界无论走哪条收尾最终都汇入同一个GC_stopped_mark标记算法本身不需要为增量版和回退版准备两套逻辑它们用的是同一个推进器区别只在什么时候停手。四、值类型数组的断点续扫4.1 为什么单个大对象能劫持时间片第三节的推进器建立在一个隐含假设上标记工作可以被切成大致均匀的小份每份对应弹出一个对象、扫完它的引用。对普通托管对象这个假设成立单个对象通常几字节到几百字节扫描耗时可以忽略。但值类型大数组打破了它。想象一个byte[4 * 1024 * 1024]的缓冲动画缓存、地形高度图、网络收包缓冲都可能是这个量级。按 2.3 节的向量扫描路径整个数组要当连续区域过一遍做保守识别。如果GC_mark_some必须一次扫完这个数组才能返回那么每帧 3ms 时间片的承诺就被这一个对象击穿了时间片再小这个数组的扫描也必须整段落在某一帧里。堆里再有若干个这种量级的数组单帧暂停会重新膨胀到几十毫秒增量等于白做。问题的根源不是数组大而是扫描进度这个状态粒度太粗普通对象扫完与否是二值的要么没弹出来要么整个扫完中间没有可保存的进度点。4.2 GC_gcj_vector_proc按 256 元素分块Boehm 的解法是给向量扫描加进度游标。GC_gcj_vector_proc一族的向量扫描函数把大数组按固定元素数分块推进Unity 这条链路上每步处理 256 个元素扫完一块就返回控制权游标留在数组内部下次从游标处续扫。示意// 示意向量扫描的分块推进对照 Boehm 向量标记函数的结构 // p_current 保存在 GC 持久状态里跨调用保留 void GC_gcj_vector_proc(void *p_current, ...) { word *base 数组起始; word *end 数组结束; word *p p_current; // 上次扫到哪里 while (p end 本次预算未用完) { word *chunk_end p 256; // 一块 256 元素 if (chunk_end end) chunk_end end; while (p chunk_end) { // 逐元素保守识别命中的引用照常打标记、压标记栈 p; } 扣减工作亏空; } p_current p; // 游标回写断点在此 }有了这个游标一个 4MB 的数组不再是一次不可分割的 3ms 停顿而是摊在若干个时间片里的连续小块。推进器的亏空记账也能正确反映真实工作量扫了 256 个元素就扣 256 个元素的钱不会出现账本上一小步、实际一大坨的失真。4.3 分块扫描的连带设计分块带来一个必须回答的问题数组内部扫描到一半时中断另一半里的引用岂不是漏了答案在第二节的不变式里游标之前的元素已经识别过的引用都已打标记压栈游标之后的元素尚未扫描但它们所属的数组对象本身已标记数组整体在标记栈的追踪之下续扫完成前标记不算收尾也就不会进入清扫。换句话说对象已标记但内容未扫完本来就是标记栈的常态向量扫描只是把这个状态从对象级细化到了数组内部偏移级对正确性框架没有增加新约束。顺带一提256 是块大小的经验取值不是可配置项。块太大则断点粒度粗块太小则循环开销与游标维护成本上升。对使用者而言它透明存在你能观察到的只是一个大数组不再单独撑爆一帧的 GC 时间这个结果。五、正确性核心脏页重扫而非写时快照5.1 问题已扫过的对象又被写入了新引用增量窗口里应用线程在跑最危险的时序是这样的时刻 T1GC 扫描对象 X读完 X 的全部引用槽X 的扫描完成 时刻 T2应用执行 X.field Y其中 Y 尚未被标记 时刻 T3GC 继续标记但 X 已弹出标记栈不会再被读取 时刻 T4标记宣告完成Y 仍是白色 时刻 T5清扫把 Y 当垃圾回收 时刻 T6程序访问 X.field拿到已释放的 Y → 崩溃或内存损坏这就是漏标。所有增量或并发标记器都必须阻断这条时序链差别只在用什么手段。经典理论给出两条路线先讲清楚它们再看 Boehm 的第三种选择。5.2 经典两条路线插入屏障与删除屏障Dijkstra 插入屏障在写入发生时检查新值。若黑对象已扫完写入了一个白对象就把白对象染灰入队保证它不被漏掉。它的口号是黑到白的边不允许存在只关心新建立的引用实现上只需要在写入指令后插入一小段检查。Yuasa 删除屏障SATB写时快照在写入发生前记录旧值。它维护的是标记开始时刻的对象图快照任何被覆盖掉的旧引用如果还没标记就补标记入队从而保证快照里所有可达对象都被保留。它的口号是标记开始后消失的引用也要算数代价是会保留一批标记期间已经实际死亡的对象也就是浮动垃圾。两条路线的共同点都在每次引用写入时做细粒度判断对象级甚至字段级用精确的队列操作换取精确的保留范围。这也是为什么很多人默认增量 GC 一定有 SATB本章开头纠正的正是这个默认。5.3 Boehm 的选择MANUAL_VDB 页粒度重扫Boehm 走了第三条路它不修改写入语义而是记录哪些内存页被写过标记完成前反复复查这些页。Unity 这条链路上启用的是 MANUAL_VDB虚拟脏位的一种实现配置见gcconfig.hUnity 所用 fork 的相关改动可追溯到 2018 年 Jonas Echterhoff 的提交。它的工作方式写屏障不改引用只做一件事计算被写指针所在的页把该页在GC_dirty_pages里标脏。增量模式下 Unity 会为托管代码生成这些屏障生成细节见 06-04 章增量标记推进过程中GC 通过GC_read_dirty消费脏页集合把脏页上的对象重新纳入扫描范围收尾时所有脏页被重扫一遍5.1 节那条时序链在 T3 与 T4 之间被打断X 所在的页在 T2 被标脏收尾重扫会重新读 X 的引用槽Y 在这一刻被发现并标记T5 的误回收不会发生。脏页复查示意 应用写入 X.fieldY ──写屏障──▶ GC_dirty_pages[X所在页] 置脏 │ GC 标记推进中周期性 GC_read_dirty ◀──┘ │ 取出脏页集合 ▼ 重扫脏页上的对象新引用补标记、重新入标记栈 │ ▼ 收尾 STW清点剩余脏页全部重扫完毕才允许转入清扫与上游 Boehm 对比一下Boehm 原生在多数平台上用的是 MPROTECT_VDB靠mprotect把堆页设为只读、缺页异常处理函数标脏不依赖每次写入插入屏障代码。Unity 选择 MANUAL_VDB换来的是跨平台行为一致、不依赖信号机制代价是每次引用写入都要付一条屏障指令。两种 VDB 都是页粒度脏页复查的语义相同。5.4 得失分析为什么页粒度重扫对 Boehm 是对的这个选择对 Boehm 是自洽的得失都很具体。得实现简单。不需要对象级队列不需要区分插入/删除语义不需要维护 SATB 快照脏页集合就是全部增量正确性状态。与保守扫描天然契合保守扫描本来就宁可多标重扫一个页大不了多标几个碰巧落在页内的值不会引入新的漏标风险。不移动对象也简化了问题页地址在整轮标记中恒定脏位不需要跟着对象搬家。失粒度粗。典型估算一页 4KB 在 64 位下容纳 512 个指针槽页内牵一发动全身哪怕只写了一个字段整页 512 个槽都要重扫。若一页上密布大对象或页内多数对象都已扫过重扫就是明显的重复劳动这就是重扫放大。极端场景下高频改写集中在少数热页同一页可能被反复标脏、反复重扫06-03 章讲的进度原地踏步直至回退相当一部分压力正来自这里的重复工作量。放在 Unity 的场景里权衡这笔交易仍然划算。游戏的托管堆里大量对象短命且分散页粒度重扫的重复成本被保守扫描的容错性和实现的简单性抵掉了大半而一个复杂的 SATB 屏障体系意味着每次引用写入更贵、可移植性更差对引擎级、双后端、全平台的约束来说不是好买卖。没有免费的午餐Boehm 只是选了最适合它自己性格的那份账单。六、初始与收尾的 STW 边界6.1 enable 时强制从干净状态开始增量窗口不能从一个混乱的堆开始。misc.c的GC_enable_incremental里有一个容易被忽略但至关重要的分支启用时若发现堆里已有分配GC_bytes_allocd 0先强制执行一次完整收集GC_gcollect_inner()把堆清干净然后再调用GC_read_dirty(FALSE)初始化脏位跟踪。为什么必须这样增量标记的正确性建立在标记从一个确定的起点出发上。如果启用瞬间堆里还残留着上一阶段积累的对象和引用关系而脏位跟踪尚未开始记录这些区域的写入那么增量窗口里这些历史遗留页上的任何写入都不会被标脏第五节的重扫机制对它们直接失效漏标风险从启用的第一帧就埋下。先做一次全量收集等于把所有对象重新归类、让所有页进入已知状态增量窗口从这张干净的底图上开始工作。这是增量模式对自身前提的一次自检也是为什么运行中途打开增量开关的那一刻会看到一次出乎意料的完整 GC 停顿。enable 路径的引擎侧全貌属于 06-01 章本节只讲它对算法起点的意义。6.2 收尾GC_stopped_mark 一次做完剩余标记增量窗口关闭时标记推进完毕或者触发回退剩余的标记工作由一次GC_stopped_mark一次性完成参数决定这次停止函数是否会因时间预算让路正常收尾用可超时的GC_timeout_stop_func回退路径用永不停止的GC_never_stop_func。收尾 STW 的职责边界要分清楚。它要做的事扫清标记栈里最后一批对象、重扫所有仍挂着的脏页这是第五节时序链的最终封口、确认标记完成。它不做的事清扫本身那是标记完成后的一次性工作不分片见 06-03 章、以及任何调度决策什么时候进入收尾、收尾失败了怎么办都是调度层的事细节同样在 06-03 章。从算法视角看收尾 STW 是增量标记给自己留的最后一致性窗口。增量窗口里所有被推迟、被合并、被粗粒度记录的欠账都在这一刻被全额结清结清之后堆上不再有任何未决的标记状态清扫才能安全开始。这一帧的长度取决于脏页数量和剩余标记栈深度典型估算仍在零点几毫秒到低毫秒量级具体由堆规模与增量期间的改写密度共同决定。七、漏标与浮动垃圾的残余风险7.1 保守扫描宁可多标不可漏标Boehm 的保守识别在增量语境下反而成了一层保险。任何落在堆地址范围内的值都会被当引用处理包括整数、浮点数、指针运算的中间值只要它们碰巧长得像指针。这意味着少量真正的漏标窗口被碰巧有值落在目标地址补上的概率不为零尤其是堆布局密集、地址连续的场景更重要的是框架性的作用脏页重扫本来就是宁滥勿缺的设计多扫一页不会致错漏扫一页才会保守扫描在识别层面延续了同样的哲学两层宁可多标叠加把增量模式下的漏标崩溃概率压到了实践中相当罕见的水平。这不是说漏标不可能第五节那条时序链依然需要脏页机制严格阻断保守扫描只是在脏页机制万一失效比如某条未走屏障的写入路径或平台差异导致的脏位遗漏时提供了一层概率性的冗余而不是确定性的保证。把冗余当保证用迟早出事。7.2 代价误保留、堆膨胀与碎片宁可多标的账单有三张。误保留。看起来像指针的值把本该死掉的对象多标成存活它会活过这一轮直到下一轮那个可疑值恰好不再出现。这直接推高了每轮的存活集合GC 看到的有效堆比真实需要的大。堆膨胀。误保留叠加上增量窗口里出生即标记的新对象标记期间分配的对象一律按存活处理避免被本轮误伤增量周期里的存活集合通常比同场景非增量模式更大。堆只能涨不能缩膨胀会延续到后续轮次。碎片。Boehm 不压缩对象永远不搬家回收后的空洞只能靠空闲链表复用。误保留制造的半死不活对象散布堆中把连续的大空洞切碎后续大分配找不到合适块时只能向系统再要内存进一步加剧膨胀。这一点与 03-02、03-04 章讲的 Boehm 碎片问题是同一件事增量模式没有缓解它只是没有让它变得更糟。7.3 一个反直觉的结论把三、四、五、六节串起来会得到一个有点反直觉的结论Unity 增量 GC 的正确性与其说靠精巧的屏障算法不如说靠粗糙但容错的整体设计。页粒度重扫牺牲了精度换来实现简单和平台一致保守扫描牺牲了识别精度换来对漏标的天然抗性新对象直接标记牺牲了回收及时性换来状态机的自洽。每一处都在用多做无用功换少犯致命错。对一个嵌在游戏引擎里、要同时跑在 Mono 和 IL2CPP 上、要覆盖从手机到主机的收集器来说这是务实到近乎必然的选择。八、算法层横向对比把 Boehm 增量与另外三代主流方案放在同一张表里。维度刻意选在算法层面而不是产品特性层面维度Boehm 增量Unity.NET 后台标记ZGC / Shenandoah中断粒度时间片分帧推进首尾短暂 STW分代分阶段标记期并发重新标记 STW几乎全程并发仅在少数安全点短暂暂停屏障类型写屏障只标脏页MANUAL_VDB不改语义卡表 SATB 风格屏障服务于分代与增量正确性加载屏障读屏障配合颜色指针对象是否移动不移动非压缩移动分代压缩移动配合染色指针转发正确性机制脏页复查 保守识别双重兜底屏障保证引用关系不丢精确识别颜色指针 加载屏障在读侧拦截失效引用适用取向嵌入式集成、跨平台一致性优先通用运行时吞吐与内存利用率优先大堆低延迟优先实现复杂度最高表里三列的差异本质上是在哪一侧付出代价。Boehm 把代价放在 GC 侧重扫放大、误保留换来托管代码侧的屏障极简和实现的可移植。.NET 把代价放在屏障侧每次引用写入更贵、卡表要维护和压缩侧移动对象要修补引用换来精确识别和更好的内存利用率。ZGC 与 Shenandoah 把代价放在读侧每次引用读取都要过加载屏障和实现复杂度上换来近乎无感知的暂停这是概念层面的对照Unity 并未采用这类机制。需要强调的是这张表比较的是算法取向不是优劣排名。Boehm 的脏页 保守组合在游戏这个特定场景里是低风险选择让它在不引入现代收集器复杂度的前提下把峰值暂停压进帧预算反过来一旦项目迁到 Unity 6 的 CoreCLR 后端评估对象就整列右移到 .NET 那套卡表与压缩的体系本文的机制分析随之整体失效。选型的正确姿势永远是先确认自己站在哪一列上。九、本章小结把全章收拢成五句话。Unity 增量 GC 的标记算法就是 Boehm 的GC_mark_some一个把标记栈和扫描游标驻留在持久内存里的可分步推进器GC_collect_a_little_inner按工作亏空驱动它任意一次返回后堆都处于可断点续扫的合法中间态。值类型大数组通过GC_gcj_vector_proc按 256 元素分块推进防止单个大对象劫持整个时间片。正确性不靠 SATB 快照靠 MANUAL_VDB 的页粒度脏记录与GC_read_dirty的持续复查用重扫兜住已扫对象又被写入新引用的漏标窗口与保守扫描的宁可多标哲学一脉相承。增量的首尾各有一个 STW 边界enable 时若堆非空则强制全量收集保证干净起点收尾时GC_stopped_mark一次结清剩余标记与脏页欠账。误保留、堆膨胀与非压缩下的碎片依然在只是被整体设计控制在可接受范围。本章总结本章从错误前提纠正出发还原了 Unity 增量标记算法的真实实现它不是 SGen 的三色改造而是 Boehm 增量模式下的页脏位复查体系。四个核心机制撑起了整个设计可分步的标记推进器GC_mark_some与亏空记账、大数组的游标化扫描256 元素分块、页粒度的脏记录与重扫MANUAL_VDB、以及首尾两处 STW 边界的状态自检。它们共同保证了每帧几毫秒的承诺在对象图持续变化的现实中依然成立。带着三个问题离开本章。为什么增量标记能断点续扫而不重头再来因为状态常驻 GC 持久内存不是每帧重建。为什么开了增量还要担心漏标崩溃因为漏标窗口由脏页重扫阻断保守扫描只是概率性冗余真正的保障在GC_read_dirty的复查纪律里。为什么 Unity 不用更先进的 SATB 或并发标记因为页粒度重扫与保守扫描这对组合在双后端、全平台、引擎级集成的约束下用最少的复杂度买到了足够的正确性这笔账在 03-04 章的架构视角和本章的算法视角下是同一个结论。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑