Changchun Master Li

用代码感受 CPython 垃圾回收机制

2017-07-08

初步探索

编程中最常见的赋值语句也很有内涵,变量名就像一个handle,间接控制对象。id函数可以获取对象的真实地址,相同地址即同一对象,(ob1 is ob2) 等价于 (id(ob1) == id(ob2))。我们可以利用id和is探索赋值语句。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# True
a = 1
b = 1
print(a is b)

# True
a = "good"
b = "good"
print(a is b)

# False 过长的字符串无缓存
a = "very good morning"
b = "very good morning"
print(a is b)

# False 引用类型
a = []
b = []
print(a is b)

但在产生代理对象时有一个例外情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def bar(this, x):
return this.xx + x

class Foo(object):
def __init__(self, xx):
self.xx = xx
bar = bar

foo = Foo(1)

# False
foo.bar is Foo
# True
id(foo.bar) == id(Foo.bar)

因为foo.bar是表达式的计算结果

回收算法

CPython Garbage Collector 有三种回收思路:

1. reference count 引用计数

1
2
3
4
5
6
7
8
9
10
import sys

a = 1
print(sys.getrefcount(a))

b = a
print(sys.getrefcount(b))

del a
print(sys.getrefcount(b))

当使用变量名传递给getrefcount()时,实际同时创建了一个函数作用域内的临时引用。因为结果会多1。当引用计数为0则释放对象。可以通过globals()查看全局引用字典。

2. mark and sweep and reference cycle 标记清除

两个对象互相引用,或一个对象自己引用自己,会产生循环引用。

1
2
3
4
5
6
7
a = []
b = [a]
a.append(b)

a = []
a.append(a)
print(getrefcount(a))

循环引用给内存释放造成了麻烦,CPython 用 标记清除算法 解决循环引用问题: 先将引用环的计数复制一个副本,遍历对象集合,操作副本,依次减一。计数值为0的对象设为unreachable,可以被回收。

3. generation collection 分代回收

内存块根据其存活时间划分为不同的集合,每个集合就是一代。Python默认定义了三代对象集合,代数越大,越惰于回收。
我们可以手动调用回收方法

1
2
# (700, 10, 10)
gc.get_threshold()

返回值代表,700是启动阈值,生成一个对象时会检查第0代有没有满;每回收十次0代对象集合,启动一次1代回收;每回收十次1代对象集合,启动一次2代回收。

诊断方法

objgraph

referrence

quora

Tags: Python
使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

扫描二维码,分享此文章