D 编程/垃圾收集器/关于更好的 GC 实现的想法
应该能够拥有不同的 GC 实现。例如
- 精确
- 并发
- 分代
- 压缩/移动
这导致了更具体的需求
一个精确的 GC 应该只标记那些从指针引用的内存。它不应该标记从非指针类型引用的内存。移动 GC 还需要修改指针值,如果一个对象被重新定位。
如果扫描一个指针,应该知道对象的起始地址。因此,应该可以获取 PLI(指针位置信息),这是继续进行的必要条件。
在并发/增量 GC 实现中,移动引用是一个问题。
Object refa = new Object; Object refb = null;
- 开始进行垃圾收集周期
- gc 扫描 refb,它为空
- gc 暂停或被另一个线程中断
- 在其他地方:refb = refa; refa = null;
- gc 继续(认为 refb 为空)
- gc 扫描 refa,它现在也为空
- GC:哦,没有指向 obj 的 ref,我可以释放它!
- 应用程序尝试引用 refb,它不为空,并崩溃
这就是需要写屏障的原因。在并发/增量 GC 中,每次写入引用都需要使旧目标引用值的扫描失效。
由于精确扫描堆栈可能是一个难题(尤其是在考虑 C 接口时,例如从 C 代码到 D 代码的回调),可以简单地保守地扫描堆栈并“固定”所有可能从堆栈引用的对象。新的Mono GC就是这么做的。
参见“保守扫描”部分。堆栈的处理方式类似于当前的标记和清除算法。
为了支持
Object.toHash()
在移动 GC 存在的情况下,需要将哈希码存储在对象本身中。哈希码可以从对象的地址获取,但由于它的地址可能发生变化,因此需要存储它。
应该考虑支持弱指针,它们是可能有用且经常被请求的功能。如果不再有指向对象的非弱指针,弱指针将被设置为 null。Java 和 C# 也支持弱指针。
语言应该允许指向原生类型,但它们不应该与 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 就无需扫描寄存器,也不需要知道哪个寄存器包含指针值。
针对并发实现。应该支持递归使用。这意味着它应该与计数器一起使用。计数器在每次调用 disable 时递增,在每次调用 enable 时递减。
针对移动/压缩型 GC 实现。
强制执行收集周期。调用将在收集周期结束后返回。在 Stop-the-world 实现中,这意味着所有线程将暂停。在并发实现中则不会。引用计数解决方案将立即返回。