在先前《CPython 中的垃圾回收机制》一文中,我们详细介绍了 Python 程序设计语言的默认实现 CPython 中完成垃圾回收的详细步骤。文中提到了有关垃圾回收流程中有关“代”(generation)的概念。本文详细介绍这种用于提升垃圾回收器性能的常见优化手段——将变量划分为多代。
为了降低每次垃圾收集所需的时间,CPython 中的垃圾收集算法使用了一种常见的优化方法:代。这个概念背后的主要思想是假设大多数对象的寿命很短,因此可以在它们创建后不久就被垃圾收集。事实证明,这与许多 Python 程序的实际情况非常接近。因为许多临时对象的创建和销毁速度非常快。一个对象的“年龄”越大,它就越不可能变得不可达。
为了利用这一事实,所有容器对象都被隔离在三个空间/代中。每个新的对象都从第一代(第 0
代)开始。垃圾回收算法只在某一代的对象上执行,如果一个对象在它这一代的收集中存活下来,它将被移到下一代(第 1
代),在那里它将被扫描从而尝试收集的次数变少。如果同一对象在这一代(第 1
代)的另一轮垃圾收集的过程中存活下来,它将被移到最后一代(第 2
代),在那里它将有最少的被 GC 程序扫描的次数。
当对象的数量达到预定义的阈值时,就会被尝试收集,这个阈值对每一代来说都是唯一的,越老的一代越低。这些阈值可以使用 gc.get_threshold
函数来检查:
>>> import gc
>>> gc.get_threshold()
(700, 10, 10)
可以使用 gc.get_objects(generation=NUM)
函数检查这些代中的内容,并且可以通过调用 gc.collect(generation=NUM)
在一个代中专门触发 GC 程序:
>>> import gc
>>> class MyObj:
... pass
...
# Move everything to the last generation so it's easier to inspect
# the younger generations.
>>> gc.collect()
0
# Create a reference cycle.
>>> x = MyObj()
>>> x.self = x
# Initially the object is in the youngest generation.
>>> gc.get_objects(generation=0)
[..., <__main__.MyObj object at 0x7fbcc12a3400>, ...]
# After a collection of the youngest generation the object
# moves to the next generation.
>>> gc.collect(generation=0)
0
>>> gc.get_objects(generation=0)
[]
>>> gc.get_objects(generation=1)
[..., <__main__.MyObj object at 0x7fbcc12a3400>, ...]
收集最老的一代
除了各种可配置的阈值外,只有当 long_lived_pending / long_lived_total
(待检查/总数)的比率高于给定值(固定为 25%
)时,垃圾收集器才会触发对于最老一代对象的完整垃圾收集过程。原因是,尽管“非完整”垃圾收集过程(仅对年轻一代和中间一代进行 GC)总是会检查大致相同数量的对象(由上述阈值决定),但完整收集的成本与最老一代对象的总数成正比,而最老一代对象的总数实际上是没有限制的。事实上有人指出,要是每创建 n
个对象(n
是自定义的常数)就进行一次完整的收集,就会在创建和存储大量最老一代对象的工作负载中导致性能的急剧下降(例如,建立一个大型的 GC 跟踪对象列表会造成平方的时间复杂度,而非预期的线性复杂度)。而使用上述比率,则会产生与总对象数量相关的均摊线性性能(其效果可以这样总结:“随着对象数量的增加,每一次完整的垃圾收集的成本越来越高,但我们收集的对象越来越少”)。