引用计数的致命缺陷与应对之策

他山之石,可以攻玉。

—— 春秋·孔子·《诗经·小雅·鹤鸣》

现代高级编程语言一般都内置垃圾回收机制,替程序员担起内存管理的重任,极大地提高了生产力。不同编程语言,采用的垃圾回收算法各有异同。那么,常见垃圾回收方法都有哪些呢?

  • 引用计数法 ( 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

1
2
3
>>> pi = 3.14
>>> pi
3.14

由于此时只有变量 pi 引用 float 对象,因此它的引用计数为 1

当我们把 pi 赋值给 f 后,float 对象的引用计数就变成了 2 ,因为现在有两个变量引用它:

1
2
3
>>> f = pi
>>> f
3.14

我们新建一个 list 对象,并把 float 对象保存在里面。这样一来,float 对象又多了一个来自 list 对象的引用,因此它的引用计数又加一,变成 3 了:

1
2
3
>>> l = [f]
>>> l
[3.14]

标准库 sys 模块中有一个函数 getrefcount 可以获取对象引用计数:

1
2
3
>>> import sys
>>> sys.getrefcount(pi)
4

咦!引用计数不应该是 3 吗?为什么会是 4 呢?由于 float 对象被作为参数传给 getrefcount 函数,它在函数执行过程中作为函数的局部变量存在,因此又多了一个引用:

随着 getrefcount 函数执行完毕并返回,它的栈帧对象将从调用链中解开并销毁,这时 float 对象的引用计数也跟着下降。因此,当一个对象作为参数传个函数后,它的引用计数将加一;当函数返回,局部名字空间销毁后,对象引用计数又加一。理解这些后,getrefcount 的行为也就解释得通了。

引用计数就这样随着引用关系的变动,不断变化着。当所有引用都消除后,引用计数就降为零,这时 Python 就可以安全地销毁对象,回收内存了:

1
2
3
>>> del l
>>> del f
>>> del pi

循环引用

对象引用关系构成一张 有向图 ,这张图可以很复杂。但如果图中有环,形成 循环应用 关系,引用计数法的表现就比较微妙了。先考察这个简单的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Car:
    
    def __init__(self, factory):
        self.factory = factory

class CarFactory:
    
    def __init__(self):
        self.cars = []
        
    def build_car(self):
        car = Car(factory=self)
        self.cars.append(car)
        return car

这个例子定义了两个类,Car 表示一辆汽车,它有一个属性 factory 指向制造它的汽车工厂;CarFactory 表示一个汽车工厂,它有一个列表 cars ,保存它制造的每辆汽车。这样一来,CarCarFactory 的实例对象构成了互相引用的关系。

接下来,我们创建一个汽车工厂实例,并通过它建造两辆汽车,看看引用关系是怎样的:

1
2
3
>>> factory = CarFactory()
>>> car1 = factory.build_car()
>>> car2 = factory.build_car()

CarFactory 通过 cars 属性引用一个列表对象,列表对象又引用两个 Car 对象,而 Car 对象又通过 factory 属性引用 CarFactory 对象。因此,上面这个引用关系图中,形成了两个 。那么,环会导致什么问题呢?点击“阅读原文”,获取更多详情!

【Python源码剖析】系列文章首发于公众号【小菜学编程】,敬请关注:

【Python源码剖析】系列文章首发于公众号【小菜学编程】,敬请关注: