跳转到内容

D 编程/垃圾收集器/关于更好的 GC 实现的想法

来自维基教科书,开放书籍,构建开放世界

应该能够拥有不同的 GC 实现。例如

  • 精确
  • 并发
  • 分代
  • 压缩/移动

这导致了更具体的需求

哪个词是指针

[编辑 | 编辑源代码]

一个精确的 GC 应该只标记那些从指针引用的内存。它不应该标记从非指针类型引用的内存。移动 GC 还需要修改指针值,如果一个对象被重新定位。

哪个地址属于哪个对象

[编辑 | 编辑源代码]

如果扫描一个指针,应该知道对象的起始地址。因此,应该可以获取 PLI(指针位置信息),这是继续进行的必要条件。

移动引用

[编辑 | 编辑源代码]

在并发/增量 GC 实现中,移动引用是一个问题。

Object refa = new Object;
Object refb = null;
  1. 开始进行垃圾收集周期
  2. gc 扫描 refb,它为空
  3. gc 暂停或被另一个线程中断
  4. 在其他地方:refb = refa; refa = null;
  5. gc 继续(认为 refb 为空)
  6. gc 扫描 refa,它现在也为空
  7. GC:哦,没有指向 obj 的 ref,我可以释放它!
  8. 应用程序尝试引用 refb,它不为空,并崩溃

这就是需要写屏障的原因。在并发/增量 GC 中,每次写入引用都需要使旧目标引用值的扫描失效。

保守堆栈扫描

[编辑 | 编辑源代码]

由于精确扫描堆栈可能是一个难题(尤其是在考虑 C 接口时,例如从 C 代码到 D 代码的回调),可以简单地保守地扫描堆栈并“固定”所有可能从堆栈引用的对象。新的Mono GC就是这么做的。

参见“保守扫描”部分。堆栈的处理方式类似于当前的标记和清除算法。

对象哈希码

[编辑 | 编辑源代码]

为了支持

Object.toHash()

在移动 GC 存在的情况下,需要将哈希码存储在对象本身中。哈希码可以从对象的地址获取,但由于它的地址可能发生变化,因此需要存储它。

弱指针

[编辑 | 编辑源代码]

应该考虑支持弱指针,它们是可能有用且经常被请求的功能。如果不再有指向对象的非弱指针,弱指针将被设置为 null。Java 和 C# 也支持弱指针。

指向原生类型和成员变量的指针

[编辑 | 编辑源代码]

语言应该允许指向原生类型,但它们不应该与 GC 相关。这意味着,它们不应该阻止 GC 删除对象,并且它们不被标记为“指针”。

因此,GC 不需要查找对象的起始位置。程序员只需要确保他不会对成员进行引用,这些成员的寿命比整个对象更长。

特定于 GC 的每个对象数据

[编辑 | 编辑源代码]

如果可以在对象内部存储有限数量的 GC 数据(例如标志),那么特定的 GC 实现可能会更有效地实现。但请注意,大多数管理数据可以存储在包含该对象的内存块中。

对象布局

[编辑 | 编辑源代码]

头脑风暴可能的具体对象布局(仅针对 GC)

类型信息指针

[编辑 | 编辑源代码]

前 4 个字节将包含一个类型信息指针,类型信息包含对象的布局信息。这种布局信息将以位图或区域列表((偏移量,长度) 对的列表)的形式存在。

双向内存布局

[编辑 | 编辑源代码]

一个非常专门的想法是将指针数据和整数数据分离,因此 GC 扫描对象不需要位图或类似的复杂数据结构。指针和整数将被完全分离

    +----------+
    + integers +
    .   ...    .
    +----------+
    + reflen   +
    +----------+   <------ object ptr
    .   ...    .
    + pointers +
    +----------+

指向对象的指针始终指向对象的中间。在对象指针之上,所有数据都是整型数据;在对象指针之下,所有数据都是指针数据。GC 可以使用 “reflen” 成员来找到需要扫描的区域的大小。

继承类可以将它们的整型/指针数据附加到基地址的数据之上/之下。如果对象被构造,则需要正确设置 “reflen” 成员。

虽然这将非常快(与位图相比),但它也可能非常难以集成到现有的编译器/环境/工具链中。使用内部指针,类头将更难找到。结构体数组也将是一个(可能可以解决的)问题。

修改后的双向内存布局

[编辑 | 编辑源代码]

也可以将指针数据组织成区域,并像单链表一样链接这些指针数据区域(例如在存在继承的情况下,指针不能分组到单个区域)。每个指针数据区域都以包含区域大小的非指针字开始,并且对象的第一个字包含指向第一个指针数据区域的指针,或者对象本身以该区域开始。

编译器支持

[编辑 | 编辑源代码]

读/写屏障

[编辑 | 编辑源代码]

一些 GC 需要在每次覆盖引用或读取引用时运行一些代码。一种灵活的方式是,为编译器提供一个函数,用于读取和写入访问。这些函数应该始终被内联和优化。例如:

___ref_assign( void * trg, void * src ){ trg = src; }
void* ___ref_read( void * src ){ return src; }

指针位置信息的管理

[编辑 | 编辑源代码]

GC 的分配函数不仅应该接收所需的内存大小,还应该接收一个位域,其中包含该内存中哪些字是引用的信息。

栈的引用信息

[编辑 | 编辑源代码]

每个栈帧都以包含引用信息的位域开始。如果 GC 扫描栈,则需要识别这些帧。

寄存器值

[编辑 | 编辑源代码]

编译器应该确保,引用值始终也存在于内存中。这样 GC 就无需扫描寄存器,也不需要知道哪个寄存器包含指针值。

GC 接口

[编辑 | 编辑源代码]

addRoot/removeRoot

[编辑 | 编辑源代码]

enable/disable

[编辑 | 编辑源代码]

针对并发实现。应该支持递归使用。这意味着它应该与计数器一起使用。计数器在每次调用 disable 时递增,在每次调用 enable 时递减。

pin/unpin

[编辑 | 编辑源代码]

针对移动/压缩型 GC 实现。

fullcollect

[编辑 | 编辑源代码]

强制执行收集周期。调用将在收集周期结束后返回。在 Stop-the-world 实现中,这意味着所有线程将暂停。在并发实现中则不会。引用计数解决方案将立即返回。

华夏公益教科书