对象图:从“还拿得到”定义活对象
本节阅读量:GC 面对的第一个问题不是“怎样释放”,而是“哪些对象已经不能释放”。
考虑 gc_live_record.lang 的外层结构:
|
|
keep 指向的 record 在许多次分配以后还要读取,所以它必须保留。循环中创建的八字段 record 没有保存到任何地方,下一步递归开始后就再也拿不到,可以回收。
“以后还会不会读”很难直接从机器代码猜出来。GC 使用一个更机械的定义:
如果从 root 出发,沿着堆对象里的引用能够到达某个对象,这个对象就是可达的,也就是本次 collection 必须保留的对象。
root 是堆外由程序直接持有的起点。本章会把它们放进独立的 shadow root stack;这一节先把 root 画成一个方框:
|
|
整数不是堆对象,扫描到这里就停止。若另一个 record 字段保存 closure,或者 closure 捕获 cell,图就会继续向后延伸。
节点和边来自已有的三类对象
第九章没有增加新的源语言 Value。堆上的节点仍然只有第六至第八章逐步加入的三类对象:
- record:每个字段都可能保存另一个堆对象;
- closure:每个 capture 都可能保存另一个堆对象;
- cell:当前值可能是整数、record 或 closure。
它们组成对象图时,真正需要跟随的边是:
| 对象 | 需要扫描的边 |
|---|---|
| record | 全部字段 |
| closure | 全部 capture |
| cell | 唯一的 current Value |
cell 仍是编译器表示“位置”的内部对象,不是源语言新增的可构造 Value。GC 能识别 cell,是因为编译器在堆上创建了它;读者不能在源程序里写 (cell ...)。
解释器不运行这套机器 collector,但它提供同一幅语义图:Env 中的名字指向 Store 的 Location,closure 保存自由变量的 Location,record 字段保存 Value。编译路径只是把 Location 落成真实 cell,并把对象之间的关系变成内存里的 tagged pointer。
所有堆指针继续共用 tag 4
第八章已经确定统一的机器表示,本章不会为了 GC 改回三套 pointer tag:
|
|
因此一个堆 Value 的形式始终是:
|
|
低三位只回答“这是不是堆对象”。要判断它是 record、closure 还是 cell,collector 会清除 pointer tag,再读取 offset 0 的 header。
runtime/value.h 中的种类保持:
|
|
header 的公式也没有变化:
|
|
低 8 位是 kind,其余高位是 payload word 数。collector 既用 kind 决定“哪些槽是边”,也用 payload count 算出对象占多少字节:
|
|
统一 tag 很重要。record? 不能只看低三位,否则 closure 和 cell 也会被误认成 record;它与 GC 一样,要继续检查 header kind。
record:所有 payload 都是 Value
一个 N 字段 record 的布局是:
|
|
每个字段都是完整的 tagged Value,所以 collector 从 offset 8 开始扫描 N 个 word。
空 record (record) 也是真实对象:
|
|
它是对象图中的一个节点,只是没有向外的边。这个只有一个 word 的布局会直接约束后面的 forwarding 设计:GC 不能假设旧对象一定有 offset 8 可以借用。
gc_empty_record.lang 的编译版本会让一个空 record 活着跨过多轮 collection,最后用 size 验证它仍然完好。先用解释器确认预期结果:
|
|
输出是 42。
closure:code pointer 不是对象图的边
一个捕获 C 个值的 closure 使用:
|
|
header 的 payload count 包含 code pointer,因为它决定整个对象的大小。但是 offset 8 保存的是 .Llambda0 之类的机器代码地址,不是源语言 tagged Value,也不指向托管堆。
所以 closure 的扫描规则必须分成两部分:
|
|
code pointer 不属于托管堆,collector 不能把它当作对象图中的 Value 边;若错误改写,closure 下次调用时就会跳到错误地址。gc_record_closure.lang 从另一方向验证对象图:一个 record 保存 closure,编译版本经历大量分配后再取出并调用它。
|
|
输出是 42。
cell:唯一 payload 是一条边
cell 的布局仍是第八章确定的 16 字节:
|
|
collector 必须扫描 offset 8。否则 closure 捕获的共享状态可能还在,cell 当前保存的 record 或 closure 却会被错误丢弃。
gc_closure_cell.lang 中,返回的 counter closure 捕获并修改 start 的 cell;程序先制造大量垃圾,最后调用 counter。结果仍为 42,说明 closure 到 cell 的边以及 cell 中的 Value 都被正确更新。
letrec 天然形成 closure 和 cell 的环
查看压力程序 IR 的前三行:
|
|
第二行让 closure 捕获 self cell,第三行又让 self cell 保存 closure:
|
|
这不是错误,而是可变递归绑定的真实表示。
引用计数看到这个环时,即使外部 root 已经消失,两个对象的计数也可能都不为零。copying collector 不靠局部计数判断生死:
- 若某个 root 仍能到达环,就复制整张可达子图;
- 若没有 root 能到达环,就一个对象也不复制,旧半区稍后整体丢弃。
因此“有没有环”不是问题,“能不能从 root 到达”才是本章统一的生死标准。后面的 forwarding 会保证遍历这个环时不会无限循环,也不会把同一对象复制多份。