现代高级编程语言一般都内置垃圾回收机制,替程序员担起内存管理的重任,极大地提高了生产力。不同编程语言,采用的垃圾回收算法各有异同。那么,常见垃圾回收方法都有哪些呢?
- 引用计数法 ( reference count ),对象记录引用次数,引用次数降为 0 时回收;
- 标记-清除法 ( mark-sweep ),从根集合出发,遍历能访问到的对象并标记,将所有未标记的对象清楚;
- 复制法 ( copying ),将内存划分为大小相同的两块,一块用完后启用另一块并将存活的对象拷贝过去,原来那块则被整体回收;
- 标记-整理法 ( mark-compact ),
- 分代回收法 ( generational-collection ),根据存活时间将对象分为若干代(如新生代和老生代),并按照不同代的特征采用最适合的回收策略;
- etc
引用计数可以说是最简单的垃圾回收方法,它能够在第一时间回收不再需要的对象,而且不会导致程序长时间停顿。由于引用计数存在一个致命缺陷,无情地限制了它的应用场景。
我们在前面章节提过,Python 对象依靠引用计数机制来回收。每个 Python 对象都包含 ob_refcnt 字段,这个字段记录对象的引用次数。那么,Python 是如何解决引用计数的致命缺陷的呢?它到底用了什么黑科技?
让我们带着这些疑问,开始本节关于引用计数法的学习。为全面研究 Python 垃圾回收机制,打下坚实的基础。
引用计数
引用计数 是计算机编程语言中的一种 内存管理技术 ,它将资源被引用的次数保存起来,当引用次数变为 0 时就将资源释放。它管理的资源并不局限于内存,还可以是对象、磁盘空间等等。
Python 也使用引用计数这种方式来管理内存,每个 Python 对象都包含一个公共头部,头部中的 ob_refcnt 字段便用于维护对象被引用次数。回忆对象模型部分内容,我们知道一个典型的 Python 对象结构如下:
当创建一个对象实例时,先在堆上为对象申请内存,对象的引用计数被初始化为 1 。以 Python 为例,我们创建一个 float 对象保存圆周率,并把它赋值到变量 pi :
|
|
由于此时只有变量 pi 引用 float 对象,因此它的引用计数为 1 :
当我们把 pi 赋值给 f 后,float 对象的引用计数就变成了 2 ,因为现在有两个变量引用它:
|
|
我们新建一个 list 对象,并把 float 对象保存在里面。这样一来,float 对象又多了一个来自 list 对象的引用,因此它的引用计数又加一,变成 3 了:
|
|
标准库 sys 模块中有一个函数 getrefcount 可以获取对象引用计数:
|
|
咦!引用计数不应该是 3 吗?为什么会是 4 呢?由于 float 对象被作为参数传给 getrefcount 函数,它在函数执行过程中作为函数的局部变量存在,因此又多了一个引用:
随着 getrefcount 函数执行完毕并返回,它的栈帧对象将从调用链中解开并销毁,这时 float 对象的引用计数也跟着下降。因此,当一个对象作为参数传个函数后,它的引用计数将加一;当函数返回,局部名字空间销毁后,对象引用计数又加一。理解这些后,getrefcount 的行为也就解释得通了。
引用计数就这样随着引用关系的变动,不断变化着。当所有引用都消除后,引用计数就降为零,这时 Python 就可以安全地销毁对象,回收内存了:
|
|
循环引用
对象引用关系构成一张 有向图 ,这张图可以很复杂。但如果图中有环,形成 循环应用 关系,引用计数法的表现就比较微妙了。先考察这个简单的例子:
|
|
这个例子定义了两个类,Car 表示一辆汽车,它有一个属性 factory 指向制造它的汽车工厂;CarFactory 表示一个汽车工厂,它有一个列表 cars ,保存它制造的每辆汽车。这样一来,Car 和 CarFactory 的实例对象构成了互相引用的关系。
接下来,我们创建一个汽车工厂实例,并通过它建造两辆汽车,看看引用关系是怎样的:
|
|
CarFactory 通过 cars 属性引用一个列表对象,列表对象又引用两个 Car 对象,而 Car 对象又通过 factory 属性引用 CarFactory 对象。因此,上面这个引用关系图中,形成了两个 环 。那么,环会导致什么问题呢?点击“阅读原文”,获取更多详情!
【Python源码剖析】系列文章首发于公众号【小菜学编程】,敬请关注: