Python的垃圾回收器如何检测循环引用?

46
我试图理解Python的垃圾回收器如何检测循环引用。查看文档时,我只看到一个声明,即除非涉及的对象具有__del__方法,否则会检测到循环引用。
如果发生这种情况,我了解到(可能不准确)gc模块会充当故障保护措施,通过(我假设)遍历所有已分配的内存并释放任何不可访问的块来释放内存。
Python如何在使用gc模块之前检测和释放循环引用的内存呢?

“但是不能保证收集包含循环引用的垃圾。”如文档所述。 - Joel Cornett
你能提供你所参考的文档页面链接吗? - Joel Cornett
1
这是我正在阅读的页面:http://docs.python.org/extending/extending.html#reference-counts - user1245262
1
我在链接的页面上找不到许可证,因此我不确定是否可以将其中较长的一段复制到SO中。而如果没有复制内容,则该链接就不符合答案的要求 - Sven Marnach
1
我不会写摘要,所以请继续。顺便说一句,我在Python源代码中找到了链接(http://hg.python.org/cpython/file/2059910e7d76/Modules/gcmodule.c)。还有其他相关的邮件列表线程链接。垃圾回收器的基本概念似乎没有改变。 - Sven Marnach
显示剩余4条评论
3个回答

38
Python是如何在使用gc模块之前检测和释放循环引用的内存?它其实不会。gc仅存在于检测和释放循环引用。非循环引用通过引用计数处理。现在,为了了解gc如何确定给定对象引用的对象集,请查看Modules/gcmodule.c中的gc_get_references函数。相关部分如下:
// Where `obj` is the object who's references we want to find
traverseproc traverse;
if (! PyObject_IS_GC(obj))
    continue;
traverse = Py_TYPE(obj)->tp_traverse;
if (! traverse)
    continue;
if (traverse(obj, (visitproc)referentsvisit, result)) {
    Py_DECREF(result);
    return NULL;
}

这里的主要函数是tp_traverse。每个C级类型都定义了一个tp_traverse函数(对于不包含任何引用的对象,如 str,则将其设置为NULL)。其中一个tp_traverse的例子是list_traverse,这是list的遍历函数:

static int
list_traverse(PyListObject *o, visitproc visit, void *arg)
{
    Py_ssize_t i;

    for (i = Py_SIZE(o); --i >= 0; )
        Py_VISIT(o->ob_item[i]);
    return 0;
}

我看到一个说法是,除非涉及到具有__del__()方法的对象,否则会检测到循环引用。

你说得对- Python的循环检测器可以检测并收集循环引用,除非它们包含具有__del__方法的对象,因为解释器没有办法安全地删除这些对象(要想理解其中的原因,可以想象一下,如果你有两个具有相互引用的__del__方法的对象,应该按照什么顺序释放它们)。

当涉及到具有__del__方法的对象时,垃圾回收器会将它们放在单独的列表中(可以通过gc.garbage访问),以便程序员可以手动“处理”它们。


1
在文档中,我看到了以下声明:“循环检测器能够检测垃圾循环并可以回收它们,只要Python中没有实现最终器(del()方法)。当存在这样的终结器时,检测器通过gc模块(具体来说是该模块中的garbage变量)公开循环。”http://docs.python.org/extending/extending.html#reference-counts.... 我理解这意味着gc是一种故障保护/较慢的方法。我是否误解了文档(我很容易误解)? - user1245262
2
@user1245262,__del__问题实际上与找到垃圾不太相关。Python确实会发现这些对象是垃圾并将它们粘贴到gc.garbage列表中。之所以不删除这些对象的唯一原因是Python不能确定安全删除它们的顺序。 - Winston Ewert
1
啊,抱歉 - 我忘记回答那个问题了。我相信这意味着:“当没有带有__del__方法的对象时,循环检测器可以回收所有东西。然而,由于循环检测器不能安全地收集存在__del__方法的对象,因此涉及这些对象的循环通过gc模块暴露出来,允许程序员手动清理它们”。 - David Wolever
4
自 Python 3.4 版本起,带有 __del__ 方法的循环引用对象将被回收。(PEP 442) - Antimony

8
Python是通过垃圾回收器实现的。Python并不会在使用垃圾回收器之前检测和释放循环引用的内存引用。常规情况下,当一个对象的引用计数达到零时,Python会立即释放大部分对象。(我说“大部分”,因为它永远不会释放小整数或已经声明的字符串。)对于循环引用,这种情况永远不会发生,所以垃圾回收器会定期遍历内存并释放循环引用的对象。当然,这都是针对CPython的。其他Python实现具有不同的内存管理(Jython = Java VM,IronPython = Microsoft .NET CLR)。

1
不完全准确。Python 不会遍历内存并释放无法访问的对象。它会遍历内存并检测引用循环,并释放其中的对象。 - Winston Ewert
1
它是否“遍历内存”?我以为它是遍历引用列表的?(或者“遍历内存”有一个我不熟悉的技术定义吗?) - David Wolever
1
gc 模块 就是 循环垃圾收集器。它不仅仅是 Python 接口,而且是实现。 - Sven Marnach
默认情况下,gc模块不在sys.modules中,而一些其他模块的实现是内置于Python中的,因此我认为gc模块只是实现。感谢更新。 - kindall

7

我认为在原问题的评论中,@SvenMarnich提供的一些链接中我找到了我正在寻找的答案:

容器对象是可以持有对其他Python对象的引用的Python对象。列表、类、元组等都是容器对象;整数、字符串等则不是。因此,只有容器对象存在成为循环引用的风险。

每个Python对象都有一个字段——*gc_ref*,对于非容器对象(我认为)它被设置为NULL。对于容器对象,它被设置为指向引用它的非容器对象的数量。

任何*gc_ref*计数大于1的容器对象(?我本来以为是0,但现在先这样吧?)都有非容器对象的引用。因此,它们是可达的,并且不会被视为不可达的内存岛。

任何由已知可达对象(即我们刚才确定具有*gc_ref*计数大于1的对象)引用的容器对象也不需要被释放。

剩下的容器对象没有被引用(除了彼此),应该被释放。

http://www.arctrix.com/nas/python/gc/ 是提供更详细说明的链接。http://hg.python.org/cpython/file/2059910e7d76/Modules/gcmodule.c是源代码的链接,其中有进一步解释循环引用检测思路的评论。


我想我的偏好可能有些古怪,但@SvenMarnich提供的链接给了我一个用自己能理解并能向他人解释的术语得出答案。David Wolever给出的解释也非常好,如果我想要在C中修改/修改或垃圾回收我创建和实现的对象,这将非常有用。 - user1245262

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接