Python内存管理与垃圾回收机制深度解析

月亮慢半拍
月亮慢半拍
发布时间:2026-10-13 | 2人浏览过
原创

本文深入解析Python的内存管理机制,重点探讨引用计数与分代垃圾回收算法的协同工作原理。文章详细说明了如何处理循环引用问题,解析了gc模块中gc_refs字段在识别不可达对象中的作用,并讨论了包含__del__方法的对象(Finalizer)在回收过程中的特殊处理策略及性能开销。

概要

在列表、元组、实例、类、字典和函数等结构中,循环引用问题客观存在。对于包含 __del__ 方法的实例,系统会以健全的方式进行特殊处理。为自定义新类型添加垃圾回收(GC)支持相对容易,且支持GC的Python版本与常规Python版本保持二进制兼容。

分代回收机制目前支持三个分代,能够正常运行。根据 pybench 的实测数据,该机制带来的性能开销约为百分之四。绝大多数扩展模块仍能正常工作(尽管标准发行版中的 new 和 cPickle 模块进行了相应修改)。此外,新增的 gc 模块可用于调试回收器及配置调试选项。

回收器设计具备跨平台可移植性。Python 的补丁版本已顺利通过所有回归测试,在运行 Grail、Idle 和 Sketch 等应用时未出现任何问题。

自 Python 2.0 及后续版本起,可移植的垃圾回收机制已内置其中。该功能默认处于开启状态。

为什么我们需要垃圾回收?

当前版本的 Python 采用引用计数来管理分配的内存。每个 Python 对象都关联一个引用计数,指示指向该对象的其他对象数量。当引用计数归零时,对象即被释放。引用计数对大多数程序运行良好,但其存在本质缺陷,即无法处理由循环引用引起的问题。循环引用最简单的示例是对象引用自身的情况,例如:

>>> l = []
>>> l.append(l)
>>> del l

此时创建的列表引用计数为 1。然而,由于它已从 Python 内部无法访问,且不再被使用,理应被视为垃圾。在现有版本的 Python 中,该列表永远不会被释放。

通常情况下,循环引用并非良好的编程实践,应尽量避免。然而,有时难以避免产生循环引用,甚至程序员可能未察觉到这一问题的存在。对于服务器等长期运行的程序,这一问题尤为棘手,因为开发者不希望服务器因无法释放访问不到的对象而耗尽内存。在大型程序中,追踪循环引用的产生源头往往十分困难。

“传统的”垃圾回收是怎样的?

传统的垃圾回收机制(如标记-清除法或停止-拷贝法)通常按以下步骤工作:

1. 找到系统的根对象。根对象包括全局环境(如 Python 中的 __main__ 模块)和堆栈上的对象。 2. 从这些对象出发,搜索所有可访问的对象。这些对象被视为“活跃”状态。 3. 释放其余所有对象。 不幸的是,该方法无法直接应用于当前版本的 Python。由于扩展模块的工作方式,Python 无法完全确定根对象集合。若根对象集合确定不准确,可能导致释放仍被引用的对象的风险。即使重新设计扩展模块,也缺乏可移植的方式来查找当前 C 堆栈上的对象。此外,引用计数为 Python 程序员提供了关于局部内存引用和终结语义的既有优势。因此,最佳方案是找到一种既能利用引用计数,又能释放循环引用的方法。

这个方法如何工作?

从概念上讲,该方法与传统垃圾回收机制相反。它旨在找出所有不可访问的对象,而非所有可访问对象。这样做更为安全,因为即使算法失败,结果也不会比不进行垃圾回收更糟(仅考虑浪费的时间和空间)。

由于我们仍在使用引用计数,垃圾回收器只需专注于查找循环引用。引用计数会处理其他类型的垃圾。首先我们观察到,循环引用只能由容器对象创建。容器对象是指可以包含其他对象引用的对象。在 Python 中,列表、字典、实例、类和元组均为容器对象的示例,而整数和字符串则不是。基于此发现,我们可以忽略对非容器对象的垃圾回收。这是一个有用的优化,因为整数和字符串等轻量级对象数量庞大。

我们的思路是记录所有容器对象。实现方法有多种,但最佳方案是利用双向链表,在对象结构中包含指针字段。这使得对象可以从集合中快速插入或删除,且无需分配额外内存空间。当容器被创建时,将其插入该集合;当容器被删除时,将其从集合中移除。

既然我们能获取所有容器对象,如何找到循环引用呢?首先,我们在容器对象中添加一个除了原有两个指针之外的另一个字段,命名为 gc_refs。通过以下步骤即可找到循环引用:

1. 对每个容器对象,将 gc_refs 的值设置为对象的引用计数。 2. 对每个容器对象,找到它引用的其他容器对象,并将它们的 gc_refs 值减一。 3. 所有 gc_refs 大于 1 的容器对象,表示被容器对象集合外的对象所引用。由于不能释放这些对象,我们将它们放入另一个集合。 4. 被移走的对象所引用的对象也不能被释放。我们将它们及其能访问到的对象从当前集合中移除。 5. 在当前集合中剩余的对象仅被该集合中的对象引用(即无法被 Python 获取,属于垃圾)。此时我们可以释放这些对象。

Finalizer的问题

我们的计划还面临一个挑战,即处理 Finalizer 的问题。Finalizer 指的是 Python 中实例的 __del__ 方法。在使用引用计数时,Finalizer 工作良好。当对象的引用计数降至 0 时,Finalizer 会在对象释放前被调用。这对程序员而言直观且易于理解。

在垃圾回收期间,调用 Finalizer 成为一个棘手的问题,尤其是在面对循环引用时。如果循环引用中的两个对象都包含 Finalizer,该如何处理?先调用哪一个?在调用第一个 Finalizer 之后,由于第二个 Finalizer 仍能访问该对象,导致其无法被释放。

由于此问题缺乏完美的解决方案,包含 Finalizer 的对象引用的循环无法被释放。相反,这些对象会被加入一个全局的“无法回收垃圾列表”中。程序应始终尝试重写代码以避免此问题。作为最后手段,程序可以读取该全局列表,并以对当前应用有意义的方式释放这些引用循环。

代价是什么?

正如人们所说,天底下没有免费的午餐。然而,这种形式的垃圾回收相当廉价。最大的代价之一是每个容器对象额外需要的三个字(word)的内存空间,以及维护容器集合的开销。对于当前版本的垃圾收集器,基于 pybench 测试,该开销大约导致速度下降百分之四。

垃圾回收器目前记录对象的三代信息。通过调整参数,垃圾回收花费的时间可以控制在极小范围内。对于某些应用,关闭自动垃圾回收并在运行时显式调用可能更为合理。然而,以默认的垃圾回收参数运行 pybench,结果显示垃圾回收花费的时间并不显著。显而易见,大量分配容器对象的应用会引起更多的垃圾回收时间。

目前的补丁增加了一个新的配置项来激活垃圾回收器。带有垃圾回收器的 Python 与标准 Python 保持二进制兼容。如果该选项关闭,对 Python 解释器的工作没有影响。

我该怎么使用它?

只需下载当前版本的 Python 即可。垃圾回收器已包含在 2.0 以后的版本中,并且默认开启。如果您使用 Python 1.5.2 版,这里有一个可能适用的旧版本补丁。如果您使用 Windows 平台,可以下载用于替代的 python15.dll。

Boehm-Demers 保守垃圾回收

该补丁对 Python 1.5.2 进行了一些修改,以使用 Boehm-Demers 保守垃圾回收。但您必须先应用此补丁。系统依然采用引用计数。垃圾回收器仅释放引用计数未释放的内存(即循环引用)。这应该能带来最佳性能。您需要:

$ cd Python-1.5.2
$ patch -p1 < ../gc-malloc-cleanup.diff
$ patch -p1 < ../gc-boehm.diff
$ autoconf
$ ./configure --with-gc

该补丁假设您已安装 libgc.a,使得 -lgc 链接选项可用(/usr/local/lib 目录也可)。如果您没有该库,请在编译前下载安装。

目前,该补丁仅在 Linux 上经过测试。在其他 Unix 机器上也许也能工作。在我的 Linux 机器上,启用 GC 的 Python 版本通过了所有回归测试。

总结

以上就是本文关于Python内存管理方式和垃圾回收算法解析的全部内容,希望对大家有所帮助。感兴趣的朋友可以继续参阅本站:Python算法输出1-9数组形成的结果为100的所有运算式、Python数据结构与算法之列表(链表,linked list)简单实现、Python算法之求n个节点不同二叉树个数等,有什么问题可以随时留言,小编会及时回复大家的。感谢朋友们对本站的支持!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表