Skip to content

算法Algorithm

分代回收与写屏障

Generational garbage collection · Remembered set · Generational write barrier

在双代停止世界模型中维护老到幼的槽记忆集,用幼代局部跟踪保持全堆可达对象,并核对提升和失败协议。

形式陈述 ​

分代回收把已分配对象划为老代 O 与幼代 Y,minor collection 只回收 Y。本页老代不移动,幼代用复制回收,整个收集过程停止 mutator。根与字段均为精确基址指针;无并发、弱引用、终结器或未登记外部句柄。对象是否年轻由运行时分区决定,不由变量名称或地址数字大小猜测。

若每次 minor 都扫描整个 O,就失去了跳过老代的主要目的。改维护记忆集 M:元素是老对象中的字段槽身份,且必须满足

{(o,f):o∈O, value(o.f)∈Y}⊆M.

M 可以有额外旧条目,但不能漏掉任何真实老→幼槽。字段值会变化,因此 M 保存槽,不保存历史目标地址;移动幼对象时需改写当前槽值。不同老字段即使指同一个幼对象,也要各自更新。

写入协议先验证槽、指针与分配状态,执行字段赋值;若源在 O 且新值在 Y,就把槽加入 M。本页把“赋值加屏障登记”作为没有安全点的不可中断步骤,收集只能在它全部完成后开始。若 M 的空间可能不足,必须在写之前预留登记能力,或失败后不可继续运行;不能已经发布老→幼指针却静默丢掉登记。

minor 时枚举全部根槽,并枚举 M 中的有效老槽。指向幼对象的槽执行 forward,指向老对象或 null 的槽无需搬移。扫描新复制的幼对象时,幼代后继继续 forward,老代后继只保留地址;并不递归遍历老对象。这里 forward 的唯一映射与对象字段修正复用旧复制算法。

完成后,根槽、M 槽和存活幼对象的指针都指向新的合法对象,旧幼空间被弃用。清理 M 只能删除当前已不指向新幼代的槽;若所有存活幼对象仍留幼代,同一个老→幼槽通常仍须保留到下一次 minor。不能每次收集结束都无条件清空 M。

直觉

只看直接指向幼代的根会漏掉 根→老对象→幼对象。写屏障把这种跨代入口登记下来,minor 就能从“外部根加跨代槽”进入幼代,而不必每轮重新走过所有老对象。

“多数对象很快死亡”是选择分代策略的经验动机,不是安全性假设。即使全部对象都长寿,算法仍须正确,只是复制和维护成本可能很高。[1] 安全性来自记忆集不漏边,不来自某个存活率百分比。引用计数持续维护每个对象的强入槽数,本页维护的却是少数跨代入口;二者不是同一计数算法。

例子与边界

一条跨代边保住两个幼对象 ​

老对象 O@800 有字段 p,根 r 指 O;幼对象 A@104、B@120、U@136 各16字节,A.next=B,B.next=null,U 无引用。执行 O.p=A 时写屏障得到 M={(O,p)}。没有根直接指向 Y。

目标幼空间从200开始。根 r 是老指针保持800;扫描 M 的 O.p,把 A 复制到200并改写该槽;扫描 A′ 的 next,把 B 复制到216;扫描 B′ 没有新对象。最终幼代占32字节,A′.next=216,O.p=200,U 被回收,M 仍含 O.p。老对象完全没有因本次收集搬家。

若故意漏掉这一次屏障,M为空,minor 不会扫描 O 的字段,于是存活幼集被误判为空,A、B 的旧空间全部弃用。之后 load(load(r).p) 取得失效地址104。根 r 并没有漏,错的是跨代入口漏了;这与旧“漏调用者根”是不同的接口故障。

记忆的是需要扫描并改写的老槽

再把 O.p=null,M 可以暂时留下旧槽。下一次 minor 扫描该槽见空值,不发现 A′,因而 A′、B′ 都会死亡,随后可移除 M 条目。多记一个当前空槽只增加扫描工作;少记一个当前老→幼槽则可能破坏安全。

提升也会制造跨代边 ​

假设 A′ 在本轮达到提升条件,被搬入 O,而 B′ 仍留 Y。虽然 mutator 没执行任何赋值,A′.next 现在已经成为老→幼槽。收集器必须把它加入 M,再恢复 mutator;仅在程序 store 指令插屏障还不够。若选择把所有存活幼对象一起提升到老代,则它们彼此的边都不跨代,但仍需核对其他遗留年轻对象。

本页 checker 主算法不做自动提升,另有显式 promote 操作:在暂停状态把选定对象移入 O,并扫描其字段登记所有幼后继;地址身份保持时这是分类变更,真实搬址还须履行旧复制算法的全部引用更新义务。两种步骤不能混称同一实现。

推论与应用

为什么可以不遍历全部老对象 ​

取任意从完整根集合可达的幼对象 y,选一条根到 y 的路径。若路径从根进入 Y 且此后全在 Y,它会由直接根扫描发现;否则看这条路径最后一次从 O 进入 Y 的边。该边的槽必须在 M 中,且后缀直到 y 全在 Y,幼代跟踪会找到 y。这样甚至覆盖路径多次跨代的情形。

反方向不要求精确:一个本已不可达的老对象仍可能在 M 里引用幼对象,于是 minor 会多保留那个幼对象。这是允许的保守保留,直到老代大收集或额外清理发现老对象死亡。故 minor 活集可能严格大于“全堆根可达幼对象”,安全证明是包含关系,不应写成两集合恒等。

记忆集可用集合精确保存槽,也可用 card table 标出含潜在老→幼边的地址范围。[2] 扫脏卡时必须按对象布局找到并扫描实际指针字段;卡位只表示“这一区域可能需要检查”,不把卡中的每个机器字都变成可移动引用。清理一张卡前要确认其覆盖范围已没有需保留的跨代边。

成本、失败和迁移 ​

设根槽 r、被检查记忆槽 m、复制幼对象字节 L、其指针字段 e。精确槽集下,minor 核心期望工作为 O(r+m+L+e),另需目标空间和转发记录;不会因为只收幼代就自动是常数暂停。card table 版本用脏卡覆盖的实际扫描字数替代 m。每次候选 pointer store 的代别判断与登记是 mutator 开销,应按写入次数另计。

数学模型要求目标容量足够。checker 为使错误可重试,先在私有目标堆和临时补丁表中完成复制,容量不足、无效记忆槽或非法指针都在提交前失败,原根、老字段、幼堆和 M 不变。这个事务式教具比原地改转发头的高效实现多用 O(L+r+m) 暂存;不能借它宣称真实 Cheney 在中途失败可免费回滚。陈旧条目可指向仍存在但已改值的老槽,不可指向已经释放的老对象;major collection 必须同时修复 M。

checker 的全堆可达性 oracle 只用于核对安全包含关系,需 O(|H|+|Fields|+r);它不属于只看幼代的核心算法。实现的输入合法性与目标区不重叠预检另扫描对象及字段,按 preflight_words 单列;合并表、区间列表和排序暂存还需 O(|H|) 额外空间。即使 preflight=False 关闭完整指针验证,目标区不重叠检查仍遍历全部已分配对象。对象区间排序还需 O(|H|log⁡|H|),不能把整个防御性检查器的墙钟成本写成只依赖幼代。记录每次转发和改槽产生线性事件日志,保存日志成本另记。没有 trace 时不复制每步整堆快照。

迁移任务:再加老槽 O.q=A。两槽都要登记,并最终都等于200,复制量仍32字节。再让一个不可达老对象 D 指 U 且其槽在 M 中,U 也被保留,复制量变48字节;这不违反安全,却说明 minor 不保证清空全部垃圾。最后提升 A、保留 B 为幼代:新 M 必須含 A.next,下一轮即使 O.p 已指老 A,也仍能保住 B。

参考资料

[1] Andrew Myers,Cornell CS 4120/5120,Memory Management and Garbage Collection,§10,页8:“Generational garbage collection”,幼代复制、跨代入口与 remembered set。本文的事务式失败协议和地址轨迹是独立教学设计。

[2] Urs Hölzle,A Fast Write Barrier for Generational Garbage Collectors,OOPSLA 1993 GC Workshop,“Introduction” 与 “Card Marking”两节,跨代槽和 card marking;作者项目索引核对日期。本文未采用其特定机器指令开销,也不把分代屏障当作并发增量标记屏障。

关系图谱8 个相邻概念 · 3 类关系

拖动节点调整位置。

显示关系

显示:依赖

  1. 前置三跳
  2. 前置二跳
  3. 前置一跳
  4. 当前条目
  5. 后续一跳
  6. 后续二跳
  7. 后续三跳
文字版关系按与当前条目的最短距离分组
类型化关系