Python 中的垃圾回收机制是如何工作的?

翻译 Summer ⋅ 于 6个月前 ⋅ 2564 阅读 ⋅ 原文地址

站点的翻译文章创建时,您将第一时间收到通知。

这是一篇社区协同翻译的文章,已完成翻译,更多信息请点击 协同翻译介绍

我将在这篇文章中讨论一下在CPython中是如何实现垃圾回收机制的。

CPython中垃圾回收的主要思路

  1. 维护引用计数器 。对于每一个对象,都有一个对于该对象的引用次数的计数器。如果这个计数器的值减为了 0 ,这就代表这个对象在程序中已经没用了,那么该对象所占用的内存就会被释放。

  2. 定期检测是否循环引用。 当引用计数器的值下降到 0 时来释放内存的机制并不适用于所有的情况。假如两个对象 AB ,其中 A 拥有对 B 的引用,B 拥有对 A 的引用。 这就称之为循环引用。在这种情况下,这两个对象也没有存在的价值了,此时 AB 都应该被垃圾回收处理。但是,这两个对象的引用计数值不为零, 所以内存会一直被占用。为了解决这个问题,CPython 通过使用算法来检测是否存在循环引用并释放循环引用中的对象。

  3. 通过启发式算法提升性能。 越晚创建的对象更可能需要被回收。 CPython引入了一个 分代回收 的概念来判断一个对象使用的相对年龄。年轻一代是指最新被创建出来的对象,而老一代则代表早前创建的对象。每个对象都确定的属于某一代。 当垃圾回收机制执行时, CPython 会优先尝试回收年轻一代的对象。CPython会定期回收老一代的对象(由启发式算法确定该回收执行的效率).
Vimiix 翻译于 6个月前

垃圾回收循环

了解CPython垃圾回收的运作周期是非常有益的。我们创建一个对象来观察垃圾回收机制的运作:

  • Python需要配置一个新的对象。为此,它调用 _PyObject_GC_Malloc,给这个对象分配内存以及将其添加到垃圾回收的第一阶段(我们称为0代)。 随即查看这个对象在0代中的数值是否超过阈值。如果确实超过阈值,而且垃圾回收机制当前没有运作,对 collect_generations 的调用随机生效进行垃圾回收。否则对象正常分配内存。

  • collect_generations 被调用,Python开始垃圾回收。这个方法算出什么阶段进行垃圾回收 (CPython默认有三代,但GC模块可以修改.。此外,年轻一代拥有低级索引, 所以0代是最年轻的一代)。Python循环所有代 (从最老到最年轻) 然后检测某一代的对象值超过阈值。如果有, 它会将所有年轻代合并到 这一代然后调用collect对这一代进行垃圾回收 。注意: Python希望最好在0代进行垃圾回收, 因为这一代拥有最年轻的对象,同样也能迭代最少。对老一代进行垃圾回收相当于收集所有对象因为对第i代的垃圾回收会使用从0到i代的所有对象。
  • collect 会对特定代进行垃圾回收。这相当于运行参考循环检测算法 (待会介绍) 然后在特定代找出一系列可得到和不可得到的对象。 这些可得到的对象会被并入下一高级的代 (也就是说,如果 collect 在第i代运行, 第i代的对象会被合并到i+1代)。对于不可获得的对象, CPython 会进行所有可能的终结器回调, 使弱ref回调,最终解除这些对象分配。

  • 最后,垃圾回收模块的内部状态会更新为collect完成它的职责。
MrGo 翻译于 5个月前

CPython的循环引用检测算法

Python试图在代内找出循环引用。将对循环引用的搜索限制在单个代之内减少了单次回收的工作量(如果这一代包含了更年轻的对象的话)。

为了找到循环引用,Python使用了young,即被垃圾回收机制所回收的代中的对象列表的头指针,并运行下列方法:

update_refs(young)
subtract_refs(young)
gc_init_list(&unreachable)
move_unreachable(young,  &unreachable)

update_refs将代内的每个对象的引用数都做了份拷贝,这样垃圾回收器在改变自己版本的引用数时就不会与真正的引用数搞混了。

之后subtract_refs会遍历被垃圾回收机制回收的代中的每一个对象i ,之后在代的列表中将被对象i所引用过的所有对象j的引用数递减。这一方法运行后,代中某个对象的引用数将与不属于这一代的对象中的某个对象的引用数相同(因为来自同一代内对象的所有引用已经被移除了)。

PhanTask 翻译于 5个月前

现在有趣的部分来了。move_unreachable 方法将young列表扫描了一遍,并将引用数为0的对象移动到unreachable列表中,并将它们的引用数改变为GC_TENTATIVELY_UNREACHABLE。引用数不为0的对象则将被标记为GC_REACHABLE,并且它们所引用的对象也将被遍历一遍并标记为GC_REACHABLE之后移动到young列表的末尾。这样它们之后也可以被遍历到。

我们将引用数为0的对象设定为暂时无法到达是由于下述原因。假设对象A已经被标记为暂时无法到达,而被某个对象B引用了。再假设对象B与A在同一代中,并且实际上可以从代的外部到达,但是B加入young列表却比A还要晚。之后A将被送到unreachable列表,同时move_unreachable将对其扫描一遍。然而,当move_unreachable 扫描B的时候,会发现B的引用数是非0的,并将其标记为GC_REACHABLE,然后遍历B引用的对象并同样将它们也标记为可以到达的对象。这样,A就也成为了GC_REACHABLE并被移动到了young列表的最后,从而它引用的对象也会被标记成为GC_REACHABLE。所以说,我们只能在move_unreachable扫描完整个young列表之后才能知道一个对象是不能到达的。

一旦整个young列表被遍历完,unreachable列表中剩下的所有项都一定是无法到达的,所以可以将它们都释放掉。 young列表中的项之后会被融合到更老的代中。

PhanTask 翻译于 5个月前

性能笔记:

  • CPython的垃圾回收器仍旧会产生全局停顿(Stop The World, STW),但比起其他实现来说已经很少了。引用数的释放增加了每次回收之间的耗时,因为无论什么时候,只要一个对象的引用数降低至0并被释放,一代中对象的数量都将会下降。因为当一代中的对象数量高于阈值时,回收将被触发,只要其中没有太多的循环引用的话,引用数的释放就会降低回收的次数。
  • 如果更年轻的对象更可能需要被垃圾回收这一假设是正确的话,在更年轻的代中运行垃圾回收器将会显著降低总的运行耗时。
  • 内存碎片会产生。引用计数将会很自然地导致内存碎片化,因为一个对象的释放意味着将会有小块的内存被添加回内存堆中。一些垃圾回收器通过将所有生存中的对象拷贝到另一个不同的内存部分并释放一整块内存部分来解决碎片化问题,但CPython并不这样做。
  • 正常执行是更慢的。无论何时,只要一个对象被分配,引用或者是释放,都会有一个额外的检查操作。这意味着与那些先正常运行,然后突然执行一次垃圾回收的跟踪垃圾回收器不同,CPython的引用数垃圾回收器将其工作贯穿到了程序的正常活动中,并且执行回收的次数更不频繁了。不幸的是,这意味着垃圾回收的总工作量更多了(因为额外增加的引用数检查操作)。
PhanTask 翻译于 5个月前

原文地址:https://www.quora.com/Python-programming...

译文地址:https://pythoncaff.com/topics/114/how-do...


本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

回复数量: 0
暂无回复~
您需要登陆以后才能留下评论!

Python 官方文档:入门教程

Python 3 标准库实例教程

Python 简明教程

Python 最佳实践指南