垃圾收集 - 标记+清除是否必须 monolithic/atomic

garbage collection - does mark+sweep have to be monolithic/atomic

本次讨论将使用micropython代码,但由于它是如此简单,我希望它对mark+sweep的一般讨论有用

Micropython 使用垃圾回收,特别是 mark-and-sweep;让我们定义它。

马克
mark 阶段,gc 遵循内存引用,字面上 marks 使用的内存块以指示它们可以从集合中访问根块。

扫一扫
标记阶段完成后,清扫器遍历整个堆,如果使用了一个内存块但没有 marked 这意味着代码无法访问它,因此它是 "freed"即标记为 免费。在 mark 阶段标记的内存块已删除该标记。

当前的实现需要一个原子调用来执行垃圾收集,也就是 gc,但我一直想知道是否可以将它分成多个调用而不是 monolithic/atomic 调用。

这将有助于减少抖动:而不是一个大的定时命中,你会有一堆较小的调用分散开来。 (这里不讨论如何 "spread out" 调用 gc 的实现细节......除非有人认为它会添加到讨论中。)

如果 gc 是 运行 "in the background" - in-between 字节码或 after pre-defined bytecodes - 那么错误点的分配(或释放)可能会导致race-condition 和堆损坏。在我们可以拆分 gc 执行之前,我们必须确定可能的竞争条件。

可以执行的两个操作是:分配和解除分配。

分配

如果用户在标记或清除阶段的中间执行分配会发生什么情况?

让我们看一个具体的代码示例

>> var1 = SomeAllocation()

标记期间分配
在上面的示例中,在 REPL 中执行了一条语句,因此对字典的任何添加都将添加到作为 GC Roots 中的条目的全局字典中。如果在扫描之前将一个条目添加到全局变量中,则不会发生任何事情 "bad":新的内存块将被 标记为 ,因为它应该如此。

问题是如果全局变量被扫描之后被修改。在那种情况下,内存块不会被 标记 因此在扫描阶段,它将被视为 "unreachable" 并被释放。 . .尽管不应该。

扫描期间分配
如果在清扫器遍历内存中的那个点之前分配了一个块,它将释放它,因为它没有 mark 中的特殊 mark ] 阶段。如果清扫器遍历该块后分配块,则不会发生任何不良情况。

解决方案

如果 gc 正在执行中,将分配的块标记为 已标记。唯一的缺点是,如果您在阶段扫描中分配并且在扫描器检查了 newly-allocated 块之后,您将使用标记有 标记 的块完成 gc .除非用户明确释放它,否则如果它变得无法访问,您将需要经过一个额外的 gc 周期来释放它。

但是有一个简单的解决方案:如果你在相位扫描期间分配,你检查扫描器的位置:如果 new-block-to-be-allocated 在它后面,不要用 标记标记它 否则,请用 标记 标记它,因为清扫器会删除 标记。这样你就不会退出 gc 标记为 mark.

的块

取消分配

如果用户在标记或清除阶段的中间执行分配会发生什么情况?

标记期间解除分配

如果在扫描引用 (parent) 块之前 释放 块,则不会发生任何事情。

如果一个块被释放并且它有children已经被标记,我们有一个不一致,因为块只有 marked 如果他们有一个 parent 也是 marked (或者 parent 是 GC root).结果是这些无法访问但已标记的块将不会被释放,直到额外的 gc 周期,因为已经 标记 ,这些 parent-less 但 标记 方块不会被相位扫描释放。

但是,我不认为这是一个问题,因为这与单体 gc 的情况没有任何不同。在单体 gc 中,您必须完成当前的 gc 周期,然后用户将调用 free(ptr),然后该块的 children 将在下一个 [=12] 期间被释放=].堆处于 "correct" 状态之前的时间不会改变。

扫描期间取消分配

如果一个块在扫描程序检查之前被 释放 ,则不会发生任何特殊情况。 free操作改变th目标方块的状态从 markedfree 然后当清扫器到达它时。 . .这里没什么可看的,只是一个空闲块。

如果一个块在清扫器检查后被释放释放操作将目标块的状态从已使用 免费 .

问题

我的分析是否正确:是否可以拆分 mark+sweep 垃圾回收?

是的,有可能。

Java 从版本 1.4 (2002) 开始就有一个 Concurrent Mark-Sweep (CMS) 收集器。它的工作原理与您描述的类似。

如果你 运行 Jython,我想你今天可以在本机利用它。