本次讨论将使用micropython代码,但由于它是如此简单,我希望它对mark+sweep的一般讨论有所帮助
Micropython使用垃圾收集,特别是标记和清除;让我们定义一下。
标记
在标记阶段,gc
跟随内存引用并逐字标记使用的内存块,以指示它们可以从根块集合中访问。
Sweep
标记阶段完成后,sweep 循环遍历整个堆,如果内存块被使用但未标记,则意味着代码无法访问它,因此它被“释放”,即标记为free。在标记阶段标记的内存块已删除该标记。
当前的实现需要一个原子调用来执行垃圾收集,gc
但我一直想知道是否可以将其拆分为多个调用,而不是一个整体/原子调用。
这将有助于减少抖动:而不是一个大的时间命中,你会有一堆较小的调用分散。(这里不讨论如何“展开”gc
调用的实现细节......除非有人认为它会增加讨论。)
如果gc
“在后台”运行——在字节码之间或在预定义字节码之后——那么在错误点的分配(或解除分配)可能会导致竞争条件和堆损坏。在我们可以拆分gc
执行之前,我们必须确定可能的竞争条件。
可以执行的两个操作是:分配和解除分配。
分配
如果用户在标记或扫描阶段的中间执行分配会发生什么?
我们来看一个具体的代码示例
>> var1 = SomeAllocation()
标记期间的分配 在
上面的示例中,在 REPL 中执行了一条语句,因此对字典的任何添加都将添加到全局字典中,该字典是GC Roots
. 如果在扫描之前将条目添加到全局变量中,则不会发生任何“坏事”:新的内存块将被标记为应有的状态。
问题是如果全局变量在扫描后被修改。在这种情况下,内存块不会被标记,因此在扫描阶段,它将被视为“无法访问”并被释放。. . 即使它不应该。
扫描期间的分配 如果在扫描
程序遍历内存中的该点之前分配了一个块,它将释放它,因为它没有标记阶段的特殊标记。如果在清扫器遍历所述块之后分配块,则不会发生任何不好的事情。
解决方案
如果gc
正在执行,则将分配的块标记为标记。唯一的缺点是,如果您在相位扫描中分配并且在扫描器检查了新分配的块之后,您将完成gc
一个带有标记的块mark。gc
除非用户明确释放它,否则如果它变得无法访问,您将需要通过一个额外的周期来释放它。
但是有一个简单的解决方案:如果在相位扫描期间分配,则检查扫描器的位置:如果要分配的新块在其后面,则不要标记它,否则,标记它有一个标记,因为清扫车会删除一个标记。这样您就不会退出gc
带有标记的块mark。
重新分配
如果用户在标记或扫描阶段的中间执行分配会发生什么?
标记期间的释放
如果在扫描引用(父)块之前释放块,则不会发生任何事情。
如果一个块被释放并且它的子节点已经被标记,我们就会出现不一致,因为只有当块的父节点也被标记(或者父节点是 a )时,才应该标记块。结果是这些无法到达但被标记的块直到一个额外的循环才会被释放,因为已经被标记,这些无父但被标记的块将不会被阶段扫描释放。GC root
gc
但是,我认为这不是问题,因为这与单片机的情况没有任何不同gc
。在单片机gc
中,您必须完成当前gc
循环,然后用户会调用free(ptr)
,然后该块的子代将在 next 期间被释放gc
。直到堆处于“正确”状态的时间量不会改变。
扫描期间的释放
如果一个块在清扫器检查之前被释放,没有什么特别的事情发生。自由操作将目标块的状态从标记变为自由,然后当清扫车到达它时。. . 这里没什么可看的,只是一个空闲块。
如果一个块在清扫器检查后被释放,则释放操作会将目标块的状态从used更改为free。
问题
我的分析是否正确:是否可以拆分标记+清扫垃圾收集?