“运行时维护终点分别运行本 RC 模型与分代 tracing 模型;两者的根、计数和回收状态不混用;分代算法的记忆集只登记老→幼槽,不记录本页的精确多重入度。”
形式陈述
分代回收把已分配对象划为老代 O 与幼代 Y,minor collection 只回收 Y。本页老代不移动,幼代用复制回收理路复制式垃圾回收Copying garbage collection · Cheney algorithm · Semispace collection · 半空间复制收集用 Cheney 的扫描区间与转发地址把存活对象复制到另一半空间,同时更新全部根和字段,保持共享与环。,整个收集过程停止 mutator。根与字段理路运行时根与堆可达性Runtime roots and heap reachability · GC root set · Heap object graph从可改写的根槽和有身份的指针字段建立具体堆图,说明哪些不可达对象可安全删除,以及可达为何不等于未来必用。均为精确基址指针;无并发、弱引用、终结器或未登记外部句柄。对象是否年轻由运行时分区决定,不由变量名称或地址数字大小猜测。
若每次 minor 都扫描整个 O,就失去了跳过老代的主要目的。改维护记忆集 M:元素是老对象中的字段槽身份,且必须满足
M 可以有额外旧条目,但不能漏掉任何真实老→幼槽。字段值会变化,因此 M 保存槽,不保存历史目标地址;移动幼对象时需改写当前槽值。不同老字段即使指同一个幼对象,也要各自更新。
写入协议先验证槽、指针与分配状态,执行字段赋值;若源在 O 且新值在 Y,就把槽加入 M。本页把“赋值加屏障登记”作为没有安全点的不可中断步骤,收集只能在它全部完成后开始。若 M 的空间可能不足,必须在写之前预留登记能力,或失败后不可继续运行;不能已经发布老→幼指针却静默丢掉登记。
minor 时枚举全部根槽,并枚举 M 中的有效老槽。指向幼对象的槽执行 forward,指向老对象或 null 的槽无需搬移。扫描新复制的幼对象时,幼代后继继续 forward,老代后继只保留地址;并不递归遍历老对象。这里 forward 的唯一映射与对象字段修正复用旧复制算法。
完成后,根槽、M 槽和存活幼对象的指针都指向新的合法对象,旧幼空间被弃用。清理 M 只能删除当前已不指向新幼代的槽;若所有存活幼对象仍留幼代,同一个老→幼槽通常仍须保留到下一次 minor。不能每次收集结束都无条件清空 M。
直觉
只看直接指向幼代的根会漏掉 根→老对象→幼对象。写屏障把这种跨代入口登记下来,minor 就能从“外部根加跨代槽”进入幼代,而不必每轮重新走过所有老对象。
“多数对象很快死亡”是选择分代策略的经验动机,不是安全性假设。即使全部对象都长寿,算法仍须正确,只是复制和维护成本可能很高。[1] 安全性来自记忆集不漏边,不来自某个存活率百分比。引用计数理路引用计数与循环垃圾Reference counting · Strong reference count · 引用计数回收按强引用槽的多重入度维护精确计数,用先保留后释放保护别名,执行零计数级联并解释循环为什么残留。持续维护每个对象的强入槽数,本页维护的却是少数跨代入口;二者不是同一计数算法。
例子与边界
一条跨代边保住两个幼对象
老对象 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 活集可能严格大于“全堆根可达幼对象”,安全证明是包含关系,不应写成两集合恒等。
记忆集可用集合理路字典、映射与集合 ADTDictionary ADT · Map ADT · Set ADT以有限偏函数统一刻画按键查询、更新和删除的字典、映射与集合接口。精确保存槽,也可用 card table 标出含潜在老→幼边的地址范围。[2] 扫脏卡时必须按对象布局找到并扫描实际指针字段;卡位只表示“这一区域可能需要检查”,不把卡中的每个机器字都变成可移动引用。清理一张卡前要确认其覆盖范围已没有需保留的跨代边。
成本、失败和迁移
设根槽 r、被检查记忆槽 m、复制幼对象字节 L、其指针字段 e。精确槽集下,minor 核心期望工作为
数学模型要求目标容量足够。checker 为使错误可重试,先在私有目标堆和临时补丁表中完成复制,容量不足、无效记忆槽或非法指针都在提交前失败,原根、老字段、幼堆和 M 不变。这个事务式教具比原地改转发头的高效实现多用
checker 的全堆可达性 oracle 只用于核对安全包含关系,需
迁移任务:再加老槽 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;作者项目索引核对日期。本文未采用其特定机器指令开销,也不把分代屏障当作并发增量标记屏障。