为什么在CPython垃圾回收器禁用时会调用析构函数?

9 投票
3 回答
900 浏览
提问于 2025-04-15 21:14

我正在试图理解CPython垃圾回收器的内部机制,特别是析构函数(也就是对象被销毁时调用的函数)是何时被调用的。目前为止,整体行为还算直观,但有一个情况让我感到困惑:

  1. 先关闭垃圾回收。
  2. 创建一个对象,然后移除对它的引用。
  3. 这个对象被销毁,并且_____del_____方法被调用。

我原以为只有在垃圾回收开启的情况下才会发生这种情况。有人能解释一下为什么会这样吗?有没有办法延迟调用析构函数呢?

import gc
import unittest

_destroyed = False

class MyClass(object):

    def __del__(self):
        global _destroyed
        _destroyed = True

class GarbageCollectionTest(unittest.TestCase):

    def testExplicitGarbageCollection(self):
        gc.disable()
        ref = MyClass()
        ref = None
        # The next test fails. 
        # The object is automatically destroyed even with the collector turned off.
        self.assertFalse(_destroyed) 
        gc.collect()
        self.assertTrue(_destroyed)

if __name__=='__main__':
    unittest.main()

免责声明:这段代码不适合用于生产环境——我已经注意到这非常依赖具体实现,并且在Jython上无法运行。

3 个回答

4

根据你对垃圾回收器的定义,CPython有两个垃圾回收器,一个是引用计数,另一个是其他的。
引用计数器一直在工作,无法关闭,因为它非常快速且轻量,不会对系统的运行时间产生显著影响。
另一个垃圾回收器(我想是某种标记清除的变种)会定期运行,可以关闭。这是因为它在运行时需要暂停解释器,这可能会在不合适的时刻发生,并消耗大量的CPU时间。
这个可以关闭的功能是为了在你需要做一些时间敏感的事情时使用,这样关闭这个垃圾回收器就不会给你带来问题。

5

这段文档在这里有说明(之前的链接是指向Python 3.5的文档部分,这里,后来被移动了),它解释了所谓的“可选垃圾回收器”实际上是一个用来收集循环垃圾的工具(这种垃圾是引用计数无法处理的)(更多信息可以查看这里)。引用计数的相关内容可以在这里找到,同时也提到了它与循环gc的关系:

虽然Python使用传统的引用计数方法,但它也提供了一个循环检测器,用来检测引用循环。这让程序员不必担心创建直接或间接的循环引用;这些循环引用是仅靠引用计数的垃圾回收的一个弱点。引用循环是指那些包含(可能是间接)对自身的引用的对象,因此循环中的每个对象的引用计数都是非零的。典型的引用计数实现无法回收任何在引用循环中的对象所占用的内存,或者那些被循环中的对象引用的内存,即使这些循环本身没有其他引用。

11

Python有两种垃圾回收机制:一种是引用计数,另一种是循环垃圾回收。后者是由gc模块来控制的。引用计数是无法关闭的,所以即使循环垃圾回收被关闭,引用计数仍然会继续工作。

当你把对象的引用设置为ref = None后,表示没有其他地方再引用这个对象了,这时它的__del__方法会被调用,因为它的引用计数变成了零。

文档中有个提示:“因为垃圾回收器补充了Python中已经使用的引用计数...” (我强调的部分)。

你可以通过让对象自己引用自己来阻止第一个情况发生,这样它的引用计数就不会变为零,比如给它一个这样的构造函数:

def __init__(self):
    self.myself = self

但是如果你这么做,第二个情况就会发生。这是因为带有__del__方法的垃圾循环不会被回收——具体可以查看gc.garbage的文档。

撰写回答