You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

垃圾回收:标记-清除算法必须为整体式/原子性的吗?

咱们这次讨论用Micropython代码来展开,因为它实现简洁,希望能给标记-清除算法的通用讨论做个参考

Micropython采用垃圾回收机制,具体是标记-清除算法;下面我来拆解下这个算法的逻辑。

garbage collection

标记阶段(Mark)

在标记阶段,gc会追踪内存引用,明确标记正在使用的内存块,以此表明这些块可以从根块集合(GC Roots)被访问到。

清除阶段(Sweep)

标记阶段完成后,清除器会遍历整个堆内存,如果某个已使用的内存块没被标记,就说明它没法被代码访问了,会被“释放”——也就是标记为空闲(free)。而标记阶段被标记过的内存块,会移除标记状态。

当前的实现要求通过原子调用执行垃圾回收(也就是调用gc),但我一直在琢磨:能不能把这个原子调用拆分成多个小调用,而不是用这种整体式的原子执行方式?

这种拆分的好处是能减少性能抖动:把一次大的性能耗时分散成多次小的调用。(本文暂时不讨论怎么“分散”gc调用的实现细节,除非有人觉得这能让讨论更有深度。)

如果让gc在后台运行——比如在字节码执行的间隙,或者预定义字节码执行完成后——那在错误时机进行内存分配(或释放)可能会引发竞争条件,甚至堆损坏。所以在拆分gc执行之前,我们得先搞清楚可能存在哪些竞争条件。

可能触发竞争的两种操作是:内存分配(Allocation)和内存释放(Deallocation)。

内存分配

如果用户在标记或清除阶段进行内存分配,会发生什么状况?

我们来看个具体的代码示例:

>>> var1 = SomeAllocation()

标记阶段的内存分配

上面这个例子是在REPL里执行的,所以对字典的任何添加操作都是针对全局字典,而全局字典是GC Roots里的一个条目。如果在全局字典被扫描之前添加新条目,不会有任何问题:新内存块会被正确标记。

问题出在全局字典被扫描之后再修改的情况。这时候新内存块不会被标记,到了清除阶段就会被当成“不可访问”的内存释放掉——但实际上它是应该被保留的。

清除阶段的内存分配

如果在清除器遍历到某个内存位置之前分配块,这个块因为没有标记阶段的特殊标记,会被释放。但如果是在清除器遍历过该位置之后分配块,就不会有异常。

解决方案

如果gc正在执行,把分配的块标记为已标记状态。唯一的小问题是,如果在清除阶段且清除器已经检查过新分配的块时进行分配,gc结束后这个块还会带着标记。除非用户显式释放它,否则如果这个块后来变得不可访问,得等到下一次gc循环才能被释放。

不过有个简单的解决办法:如果是在清除阶段进行分配,检查清除器的当前位置:如果待分配的新块在清除器的后方,就不用标记;否则就标记,因为清除器已经移除了该位置的标记。这样gc结束后就不会有残留的带标记块了。

内存释放

如果用户在标记或清除阶段进行内存释放,会发生什么?

标记阶段的内存释放

如果在引用(父)块被扫描之前释放某块,不会有任何问题。

但如果释放的块里包含已经被标记的子块,就会出现不一致性——因为只有当块的父块也被标记(或者父块是GC root)时,这个块才应该被标记。结果就是这些不可访问但已标记的块要等到下一次gc循环才会被释放,因为这些没有父块但已标记的块会……

内容的提问来源于stack exchange,提问作者Bob

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 07:19:14