通常,您在编写Python代码时无需担心垃圾收集器和使用内存的问题。 一旦不再需要对象,Python就会自动从它们下面释放内存。 尽管如此,了解GC的工作原理将有助于您编写更好的代码。
内存管理器
与其他流行语言不同,Python删除对象后不会立即将所有内存释放回操作系统。 相反,它使用为小对象(大小小于512字节)设计的附加内存管理器。 为了处理此类对象,他分配了大块内存,将来会在其中存储许多小对象。
一旦删除了其中一个小对象-它下面的内存就不会进入操作系统,Python会将其留给具有相同大小的新对象。 如果分配的内存块之一中没有剩余的对象,则Python可以将其释放到操作系统。 通常,当脚本创建许多临时对象时释放块。
因此,如果长时间运行的Python进程开始随着时间消耗更多的内存,则这并不意味着您的代码存在内存泄漏问题。 如果您想了解更多有关Python中的内存管理器的信息,可以在
我的另一篇文章中了解它。
垃圾收集算法
标准python解释器(CPython)一次使用两种算法,即引用计数和世代垃圾收集器(以下称为GC),这是Python的标准
gc模块 。
链接计数算法非常简单有效,但是它有一个很大的缺点。 他不知道如何定义循环引用。 因此,在python中有一个额外的收集器,称为世代GC,用于监视具有潜在循环引用的对象。
在Python中,引用计数算法是基本的,不能禁用,而GC是可选的,可以禁用。
链接计数算法
链接计数算法是最简单的垃圾收集技术之一。 一旦不再引用对象,它们将被删除。
在Python中,变量不存储值,但充当对对象的引用。 也就是说,当您将值分配给新变量时,首先会创建一个具有该值的对象,然后该变量才开始引用它。 多个变量可以引用一个对象。
Python中的每个对象都包含一个附加字段(引用计数器),该字段存储指向该对象的链接数。 只要有人引用一个对象,该字段就会增加一。 如果由于某种原因链接消失了,那么该字段将减少一。
链接数增加时的示例:
- 赋值运算符
- 传递参数
- 在工作表中插入一个新对象(该对象的链接数量增加)
- 形式foo = bar的构造(foo开始与bar指代同一对象)
一旦特定对象的参考计数器达到零,解释器就会开始销毁该对象的过程。 如果远程对象包含指向其他对象的链接,则这些链接也将被删除。 因此,去除一个物体可能需要去除其他物体。
例如,如果删除列表,则其所有元素中的引用计数都将减少一。 如果列表中的所有对象未在其他任何地方使用,则它们也将被删除。
在函数,类和块之外声明的变量称为全局变量。 通常,此类变量的生命周期等于Python进程的生命周期。 因此,对全局变量引用的对象的引用数量永远不会降为零。
在块内声明的变量(函数,类)具有局部可见性(即,仅在块内可见)。 python解释器一旦退出该块,它就会破坏其中的局部变量创建的所有链接。
您始终可以使用
sys.getrefcount
函数检查链接数。
链接计数器的示例:
foo = []
标准解释器(CPython)使用引用计数器的主要原因是历史性的。 目前,有关此方法的争论很多。 有人认为,如果没有链接计数算法,垃圾收集器的效率会更高。 该算法存在许多问题,例如循环链接,阻塞线程以及内存和cpu的额外开销。
该算法的主要优点是,不需要的对象会立即删除。
可选垃圾收集器
当我们已经有引用计数时,为什么需要额外的算法?
不幸的是,经典的链接计数算法有一个很大的缺点-它不知道如何找到循环链接。 当一个或多个对象相互引用时,就会发生环回。
两个例子:

如您所见,第一个对象引用自身,而
object1
和
object2
引用。 对于此类对象,参考计数将始终为1。
Python演示:
import gc
在上面的示例中,del指令删除了对我们对象的引用(而不是对象本身)。 Python执行del语句后,这些对象将无法从Python代码访问。 但是,关闭gc模块后,它们仍将保留在内存中,因为 他们有通函,计数器仍然是一。 您可以使用
objgraph
库以可视方式探索此类关系。
为了解决此问题,在Python 1.5中添加了称为
gc模块的其他算法。 唯一的任务是删除不再可以从代码访问的循环对象。
环回只能发生在“容器”对象中。 即 在可以存储其他对象的对象中,例如列表,字典,类和元组。 除了元组,GC不会跟踪简单和不可变的类型。 当满足某些条件时,某些元组和词典也将从跟踪列表中排除。 对于所有其他对象,保证可以应对参考计数算法。
触发GC时
与参考计数算法不同,循环GC不能实时工作,而是定期运行。 收集器的每次运行都会在代码中创建微暂停,因此CPython(标准解释器)使用各种启发式方法来确定垃圾收集器的频率。
循环垃圾收集器将所有对象分为3代(Generations)。 新对象属于第一代。 如果新设施在垃圾收集过程中仍然存在,那么它将转移到下一代。 生成的次数越高,对其进行垃圾扫描的频率就越少。 由于新对象的寿命通常很短(是临时的),因此与已经经历了多个垃圾收集阶段的对象相比,对它们进行更多访问是有意义的。
每个世代都有一个特殊的计数器和一个响应阈值,达到该阈值时将触发垃圾收集过程。 每个计数器存储分配数量减去给定代中的重新分配数量。 只要在Python中创建了任何容器对象,它就会检查这些计数器。 如果条件有效,则垃圾收集过程开始。
如果几代或更多世代一次超过阈值,则选择最高级。 这是由于以下事实:较早的几代人还会扫描所有以前的人。 为了减少长寿命对象的垃圾回收暂停次数,最老的一代还具有
一组附加条件 。
世代的标准阈值分别设置为
gc.get_threshold gc.set_threshold
700, 10 10
,但是您始终可以使用
gc.get_threshold gc.set_threshold
来更改它们。
循环搜索算法
循环搜索算法的完整描述将需要单独的文章。 简而言之,GC会遍历选定世代中的每个对象,并暂时删除单个对象中的所有链接(此对象引用的所有链接)。 经过一整遍之后,所有链接数少于两个的对象都被认为无法从python访问,并且可以删除。
为了更深入地理解,我建议阅读(译者注:英文资料)
来自Neil Schemenauer的算法的
原始描述以及来自
CPython来源的
collect
函数。
Quora的描述以及
有关垃圾收集器的
帖子也可能很有用。
值得注意的是,自Python 3.4起,算法原始说明中描述的析构函数问题已得到解决(
PEP 442中有更多详细信息)。
优化技巧
循环通常发生在现实生活中的任务中;它们可以在图形,链接列表或需要跟踪对象之间关系的数据结构中发现。 如果您的程序负载很大并且需要延迟,那么最好避免循环。
在您有意使用圆形链接的地方,可以使用“弱”链接。 弱链接是在
weakref模块中实现的,与常规链接不同,它不会以任何方式影响链接计数器。 如果事实证明具有弱引用的对象被删除,则返回
None
。
在某些情况下,禁用gc模块的自动构建并手动调用非常有用。 为此,只需调用
gc.disable()
,然后手动调用
gc.collect()
。
如何查找和调试循环链接
调试循环可能很痛苦,尤其是在您的代码使用许多第三方模块的情况下。
gc模块提供了有助于调试的帮助程序功能。 如果GC参数设置为
DEBUG_SAVEALL
标志,则所有不可访问的对象都将添加到
gc.garbage
列表中。
import gc gc.set_debug(gc.DEBUG_SAVEALL) print(gc.get_count()) lst = [] lst.append(lst) list_id = id(lst) del lst gc.collect() for item in gc.garbage: print(item) assert list_id == id(item)
一旦确定了问题点,就可以使用
objgraph对其进行可视化。

结论
主要的垃圾收集过程由链接计数算法执行,该算法非常简单并且没有设置。 附加算法仅用于搜索和删除具有循环引用的对象。
您不应该对垃圾收集器的代码进行过早的优化;在实践中,垃圾收集的问题很少见。
PS:我是本文的作者,您可以提出任何问题。