和 Java 不同,引用计数是 Python 主要的垃圾回收算法(下文均指 CPython 解释器)。需要首先明确的是,包括 Python 在内的任何高级语言的垃圾回收机制都是非常复杂的,实际上,Python 的垃圾回收也包含了分代机制等复杂的概念,本文只关注引用计数这方面的内容。

问题描述

引用计数 GC 算法,简单来说,就是当一个对象被引用的次数为 0 时,才进行回收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class A:
def __init__(self):
print('Object created')

def __del__(self):
print('Object deleted')

def func():
a = A() # 对象 A 实例被局部变量 a 引用,引用计数为 1
return a

b = func() # 对象 A 实例被全局变量 b 引用,引用计数为 2

print('exit')
1
2
3
4
alrisha@Pisces:~/code/python$ python script.py
Object created
exit
Object deleted

注意到由于 Python 全局变量的作用域,对象 A 的实例最终在程序结束时才被删除。

引用计数算法有非常多的优点,其占用内存空间小,而由于计数为 0 时即可触发 GC,因此也几乎没有时间上的停顿(几乎没有 STW,Stop the World),而其本身实现也非常容易。但是,它相对于基于 GC Root 的可达性算法(如 JVM)有一个致命缺陷,那就是难以处理循环引用。实际上,Python 引入分代机制的原因之一就是为了解决循环引用的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class A:
def __init__(self, obj = None):
self.ptr = obj
print('Object A created')

def __del__(self):
print('Object A deleted')

class B:
def __init__(self, obj = None):
self.ptr = obj
print('Object B created')

def __del__(self):
print('Object B deleted')

a = A()
b = B()

a.ptr = b
b.ptr = a

del a

print('exit')
1
2
3
4
5
6
alrisha@Pisces:~/code/python$ python script.py
Object A created
Object B created
exit
Object A deleted
Object B deleted

可以看到,由于存在循环引用,即使我们手动删除了对象 A 的引用,它依然没有被回收。

当然,在小的 Python 项目中,可能无需关注这些细节,因为 Python 的垃圾回收机制最终几乎总是能处理类似的问题。但当遇到占用内存较大的项目并且存在硬件限制的场景时,循环引用带来的问题甚至可能类似于内存泄漏。由于每轮 GC 必定存在时间间隔,在两次 GC 间隔之间有可能存在非常多的循环引用对象,从而引发 OOM。

解决方案

弱引用(不推荐)

弱引用是解决引用计数法中循环引用问题的一种方法,比较出名的例子就是 C++ 的智能指针。Python 提供了 weakref 模块来实现弱引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import weakref

class A:
def __init__(self, obj = None):
self.ptr = obj
print('Object A created')

def __del__(self):
print('Object A deleted')

class B:
def __init__(self, obj = None):
self.ptr = obj
print('Object B created')

def __del__(self):
print('Object B deleted')

a = A()
b = B()

a.ptr = weakref.ref(b)
b.ptr = weakref.ref(a)

del a

print('exit')
1
2
3
4
5
6
alrisha@Pisces:~/code/python$ python script.py
Object A created
Object B created
Object A deleted
exit
Object B deleted

我们可以认为,Python 的引用计数器仅记录强引用对象而不记录弱引用对象,因此恰当地使用弱引用能够很好地解决循环引用问题。

然而,我不推荐在 Python 中使用弱引用来解决循环引用问题。

  • 首先,Python 弱引用所使用的装饰器模式 API 在开发中添加了额外的复杂度。
  • 其次,我认为 Python 中的弱引用,相当于开发者将对象所有权完全交给了 Python 解释器。这使得对象的生命周期完全不可控(在没有 GC 的 C++中这一点没那么明显)。
  • 最后,使用弱引用也给开发者带来不小的心智压力,开发者必须时刻注意弱引用对象的判空,甚至会徒增很多不必要的代码量。

手动关闭对象

传统的方法可能才是最好的选择。面对循环引用手动清理对象引用无疑是最直观有效的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class A:
def __init__(self, obj=None):
self.ptr = obj
print('Object A created')

def finish(self):
self.ptr = None

def __del__(self):
self.ptr = None
print('Object A deleted')


class B:
def __init__(self, obj=None):
self.ptr = obj
print('Object B created')

def finish(self):
self.ptr = None

def __del__(self):
print('Object B deleted')


a = A()
b = B()

a.ptr = b
b.ptr = a

a.finish()
b.finish()

del a

print('exit')
1
2
3
4
5
6
alrisha@Pisces:~/code/python$ python script.py
Object A created
Object B created
Object A deleted
exit
Object B deleted

那这里大伙就要问了,既然要手动关闭对象,那我还用 Python 干嘛呢?这不脱裤子放屁嘛?

对于脚本和科研用途来说,确实,这些 GC 上的细节无需特地在意。但对于希望使用 Python 进行大规模工程开发的人来说,这些问题还是非常关键的。另外,上边这个例子还是不够 Pythonic ,我们完全可以用 with 语法糖,通过 Python 类的上下文管理器来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class A:
def __init__(self, obj=None):
self.ptr = obj
print('Object A created')

def __enter__(self):
pass

def __exit__(self, exc_type, exc_val, exc_tb):
self.ptr = None

def __del__(self):
print('Object A deleted')


class B:
def __init__(self, obj=None):
self.ptr = obj
print('Object B created')

def __enter__(self):
pass

def __exit__(self, exc_type, exc_val, exc_tb):
self.ptr = None

def __del__(self):
print('Object B deleted')


a = A()
b = B()


with a:
a.ptr = b
with b:
b.ptr = a

del a

print('exit')
1
2
3
4
5
6
alrisha@Pisces:~/code/python$ python script.py
Object A created
Object B created
Object A deleted
exit
Object B deleted

所以说,如果希望通过 Python 进行大规模工程开发,还是需要对 Python 的 GC 机制有一定的了解,多用 with 来关联对象的上下文。不要因为 Python 看起来简单而轻视其中的工程细节。