垃圾收集

从本质上讲,Python 的垃圾收集器(从 3.5 开始)是一个简单的引用计数实现。每次引用对象(例如,a = myobject)时,该对象(myobject)上的引用计数都会递增。每次删除引用时,引用计数都会递减,一旦引用计数达到 0,我们就知道没有任何内容可以引用该对象,我们可以释放它!

关于 Python 内存管理如何工作的一个常见误解是 del 关键字释放了对象内存。这不是真的。实际发生的事情是 del 关键字只是递减对象 refcount,这意味着如果你调用它足够多次以使 refcount 达到零,那么对象可能被垃圾收集(即使实际上仍然存在对代码中其他地方可用的对象的引用) )。

Python 在第一次需要它时就会创建或清理对象如果我执行赋值 a = object(),那时就会分配对象的内存(cpython 有时会重用某些类型的对象,例如引擎盖下的列表,但主要是它不保留一个免费的对象池,并在你需要时执行分配)。同样,只要 refcount 减少到 0,GC 就会清除它。

世代垃圾收集

在 1960 年代,John McCarthy 在实现 Lisp 使用的引用计算算法时发现了引用垃圾收集的致命缺陷:如果两个对象在循环引用中相互引用会发生什么?你怎么能垃圾收集这两个对象,即使它们没有外部引用,如果它们总是指向彼此?此问题还扩展到任何循环数据结构,例如环形缓冲区或双向链表中的任何两个连续条目。Python 尝试使用另一种称为 Generational Garbage Collection 的垃圾收集算法稍微有趣的转折来解决这个问题。

实质上,每当你在 Python 中创建对象时,它都会将其添加到双向链接列表的末尾。有时 Python 会循环遍历此列表,检查列表中对象所引用的对象,如果它们也在列表中(我们将看到它们可能不会出现的原因),则进一步减少它们的引用。此时(实际上,有一些启发式方法可以确定事情何时被移动,但让我们假设它是在单个集合之后保持简单)任何仍然具有大于 0 的引用计数的东西都会被提升到另一个名为“第 1 代”的链表(这就是为什么所有对象并不总是在第 0 代列表中),这种循环应用的次数较少。这是世代垃圾收集的用武之地。默认情况下,Python 中有 3 代(三个链接的对象列表): 第一个列表(第 0 代)包含所有新对象; 如果 GC 循环发生并且没有收集对象,它们将被移动到第二个列表(第 1 代),如果 GC 循环发生在第二个列表上并且仍未收集它们,则它们将被移动到第三个列表(第 2 代) )。第三代列表(称为“第 2 代”,因为我们是零索引)比前两个更少地进行垃圾收集,这个想法是如果你的对象是长寿的,那么它不太可能被 GCed,并且可能永远不会在应用程序的生命周期内进行 GC 操作,因此在每次运行 GC 时都没有浪费时间检查它。此外,观察到大多数对象相对较快地被垃圾收集。从现在开始,我们将这些好东西称为年轻人。这被称为“

快速搁置:与前两代不同,长期存在的第三代清单不是定期垃圾收集。检查长寿命待决对象(第三代列表中的那些,但实际上还没有 GC 循环)与列表中的总长寿命对象的比率大于 25%。这是因为第三个列表是无限的(事物永远不会从它移到另一个列表中,因此它们只在实际上被垃圾收集时才会消失),这意味着对于创建大量长寿命对象的应用程序,GC 循环在第三个列表上可以得到很长时间。通过使用比率,我们实现在对象总数中的摊销线性表现; 也就是说,列表越长,GC 越长,但我们执行 GC 的次数越少(这里是 由 MartinvonLöwis 撰写的关于此启发式的 2008 年原始提案 ,以供进一步阅读)。在第三代或成熟列表上执行垃圾收集的行为称为完全垃圾收集

因此,通过不要求我们一直扫描不太可能需要 GC 的对象,分代垃圾收集可以加快速度,但是它如何帮助我们打破循环引用呢?事实证明,可能不太好。实际打破这些参考周期的功能开始如下

/* Break reference cycles by clearing the containers involved.  This is
 * tricky business as the lists can be changing and we don't know which
 * objects may be freed.  It is possible I screwed something up here.
 */
static void
delete_garbage(PyGC_Head *collectable, PyGC_Head *old)

分代垃圾收集的原因在于我们可以将列表的长度保持为单独的计数; 每次我们向生成添加一个新对象时,我们都会增加此计数,并且每当我们将一个对象移动到另一代或解除它时,我们就会减少计数。理论上在 GC 循环结束时,这个计数(前两代反正)应该总是为 0.如果不是,那么剩下的列表中的任何东西都是某种形式的循环引用,我们可以放弃它。但是,还有一个问题:如果剩下的对象上有 Python 的魔法方法 __del__ 怎么办?每当 Python 对象被销毁时都会调用 __del__。但是,如果循环引用中的两个对象具有 __del__ 方法,我们无法确定销毁其中一个不会破坏其他对象 __del__ 方法。

class A(object):
    def __init__(self, b=None):
        self.b = b
 
    def __del__(self):
        print("We're deleting an instance of A containing:", self.b)
     
class B(object):
    def __init__(self, a=None):
        self.a = a
 
    def __del__(self):
        print("We're deleting an instance of B containing:", self.a)

我们设置一个 A 实例和一个 B 实例指向另一个,然后它们最终进入同一个垃圾收集周期?假设我们随机选择一个并且首先取消我们的 A 实例; 将调用 A 的 __del__ 方法,它将打印,然后 A 将被释放。接下来我们来到 B,我们称之为 __del__ 方法,然后哎呀! 段错误! A 不再存在。我们可以通过首先调用剩下的 __del__ 方法来解决这个问题,然后做另一个传递来实际解除所有内容,但是,这引入了另一个问题:如果一个对象 __del__ 方法保存了另一个即将被 GCed 的对象的引用怎么办?在其他地方有我们的参考?我们仍然有一个参考周期,但现在实际上 GC 不可能是对象,即使它们已经不再使用了。请注意,即使一个对象不是循环数据结构的一部分,它也可以用自己的 __del__ 方法恢复自身; Python 确实对此进行了检查,如果在调用 __del__ 方法后对象引用计数增加,则会停止 GCing。

CPython 处理这个问题的方法是将那些不具有 GC 功能的对象(任何带有某种形式的循环引用和 __del__ 方法的对象)粘贴到一个无法收集的垃圾的全局列表中,然后将它留在那里永恒:

/* list of uncollectable objects */
static PyObject *garbage = NULL;