Skip to content

模型Model

运行时根与堆可达性

Runtime roots and heap reachability · GC root set · Heap object graph

从可改写的根槽和有身份的指针字段建立具体堆图,说明哪些不可达对象可安全删除,以及可达为何不等于未来必用。

形式陈述 ​

根是位置,堆边是字段 ​

固定一次停止世界的快照。按可变存储语义,堆 H 是有限对象表,每个对象具有唯一基址、描述符和若干字段;这里只允许对象基址指针,不允许整数伪造指针。字段类型由可信布局给出,只有受管理指针字段进入引用图。

一个根槽是收集器能读取、必要时改写的位置,例如某个活动帧的局部槽、一个已保存寄存器槽或全局句柄槽。设根槽集合为 S,value(s) 为槽中保存的对象地址或空值。根槽不是对象本身:两个槽可以同时保存同一个地址,移动回收时两个位置都需要处理。

对象图采用有向图页中允许自环和平行弧的变体。每条弧以字段槽 (对象,字段编号) 为身份,起点是拥有该槽的对象,终点是槽内非空引用。递归闭包的环境可以指回闭包自身;同一 Pair 的两个字段也可以指向同一对象。把两条同端点弧合并或不合并都不改变可达对象集合,却会改变“要更新多少个指针槽”的任务。

从 value(S) 出发沿零条或更多字段弧可达的对象组成 R。未在 R 中的已分配对象就是本模型本次可回收的对象。查根需要覆盖所有能恢复计算的状态:当前函数、暂停的调用者、全局引用、运行时句柄;若解释器另存操作数栈、闭包临时量或续延,它们也属于计算状态。

直觉

一个函数返回了,并不说明它创建的全部对象都可释放;一个对象形成了引用环,也不说明它一定要保留。运行时收集的判断入口是:暂停中的程序还能从哪里拿到对象引用,并沿哪些字段继续找到其他对象?

例子与边界

一个不能漏掉调用者的快照 ​

共享计数器有如下已分配对象,箭头都表示字段内保存的引用:

text
P -> A, B       A -> Ei       B -> Ep
Ei -> C         Ep -> C       C.value = 7
U.value = 999

P 是 (inc,peek) 对,A、B 为两个闭包,Ei、Ep 为各自环境。make 已返回。main 从 P 取出 a、b 后不再需要 P;调用 a(3) 已进入 inc,A 本身也不再被以后指令使用,因为当前代码位置与环境已经保存。inc 写完 7、尚未返回时暂停;main 返回后仍要调用 b。

在固定根映射下,槽 main.b 保存 B,槽 inc.env 保存 Ei;其余旧位模式即使还留在死槽,也不是本次根。起点是 {B,Ei},从 B 到 Ep,从 Ei 和 Ep 到同一个 C,所以

R={B,Ei,Ep,C}.

A、P、U 已无从取得,可以回收。C 不必直接放在根槽中;经环境间接可达就足够。反之,若只扫描当前 inc 的环境,得到的仅是 {Ei,C},B 与 Ep 就被错误遗漏。调用者还没执行的 b(0) 是根活跃的原因,不能只搜索当前函数体出现的变量名。

从整个暂停计算取根,而非只看当前帧

按对象布局,B 占 24 字节,其余三个各占 16 字节,存活部分为 72 字节;全部已分配对象为 136 字节,所以本次可释放 64 字节。可达性计的是对象身份,Ei 和 Ep 同指 C 不会把 C 计费两次。

物理回收与静态分析不同 ​

抽象垃圾回收使用相似的可达闭包,目的是从某个分析状态中删掉失效的抽象存储事实,提高静态精度。本页 H 的节点是这次执行的具体对象,根是实际暂停位置,删除会释放实际存储。抽象槽可能合并多次分配的值,不能把它当作一个可直接释放的物理块。

在精确运行时模型里,描述符与根映射告诉我们哪些位置真是引用。保守扫描则可能把“看起来像地址”的整数也当根,因此通常多保留对象;它不能仅凭数值猜测任意改写这些位置,否则可能把普通整数改坏。本页移动收集不采用这种猜测。

弱引用、终结器、对象复活以及与外部代码的交互需要补充可达阶段和句柄协议。多线程或并发收集还需要协调快照与新写入。本页将它们排除,是为了让上述首次读取论证有可检查的前提,而不是把它当作所有 GC 的完整规范。

推论与应用

不可达为什么足以删除 ​

安全论证需要一条明确的“引用从哪里来”规则:以后获得旧对象引用,只能复制现有根值、从可达对象的字段中读出,或从已经登记的外部句柄取得;新分配只产生新对象身份。计算期间不得从整数、悬空指针、未登记的外部代码中重新造出旧对象引用。

假设未来第一次读取某个被删对象 O。用于这次读取的指针不可能凭空出现;沿其来源回溯,若来自暂停时的根,则 O 已可达;若来自字段读取,则先得到该字段所属对象,再沿字段到 O,同样形成根路径;若来自新分配,它指向的是新身份而非旧 O。三种情况都与 O 在暂停时不可达矛盾。因此删除 H\R 不改变后续合法读取结果。

写入也不会突然把一个不可达旧对象救活:要把它的地址写入可达对象,程序必须先持有那个地址,而持有的位置本来就应算根或可达字段。地址数值可以被重用,但新分配产生的是新身份;一个允许读取悬空地址的语言不满足上述前提。

这个引理证明的是安全性,不是“全部未来无用对象都会被收走”。令 keep 指向一个大数组,随后无限执行不再读取它的循环;只要所用根映射仍把 keep 保留,数组就可达,收集器会留下它。静态活跃性、环境裁剪可以减少这类保留,但仅凭图遍历无法解决所有未来是否使用的问题。

计算任务与迁移 ​

给定 r 个根槽、v 个可达对象、e 个可达指针字段,用带已访问标记的队列或栈,每个根读一次、每个可达对象扫描一次,时间为 O(r+v+e),辅助空间至多 O(v)。这是求 R 的成本;标记清扫再遍历已分配对象找死亡者,复制收集则迁移活对象并整体弃用旧区。维护可分配空间的成本由各自算法承担。

迁移题一:删掉 main.b,但新增一个全局槽 g=B。结果仍是四个活对象;变化的是根位置,不是堆图。再让 g=null,只剩 {Ei,C}。

迁移题二:增加一个不可达环 X.next=Y, Y.next=X。它们各有一个入引用,仍都不可从根取得,因而都可回收。给 X 添加一个根以后,X、Y 同时进入 R。再试只有 X 且 X.next=X 的单点自环,结论同样由是否有根决定。若图算法排除了自环,或把“有入边”误当作活跃判据,就会在这些变体上失败。

参考资料

[1] Andrew Myers,Cornell CS 4120/5120,Memory Management and Garbage Collection,页 1 的对象图与根讨论、§4 “Garbage collection via traversal”、§5 “Finding pointers”。本页根槽图和安全引理为显式受限模型下的推导。

[2] LLVM,Garbage Collection Safepoints in LLVM, “Overview & Core Concepts” 的引用副本、对象归属和更新三项要求,2026-10-08 核查。

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

拖动节点调整位置。

显示关系

显示:依赖

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