Dantangfan

python 的 GC

python 可以自动的回收垃圾,但也会出现内存泄漏。经常看到文章说在实现自己的类时,不要重定义 __del__() 函数,为什么呢? python 有些什么样的垃圾回收机制呢?看来还是有必要了解一下这方面的原理.

简介

目前我们使用的常见语言在内存分配上都支持三种形式

垃圾回收,主要就是回收的动态分配的内存.

引用计数

在 python 中,主要通过引用计数的方法来释放内存.顾名思义,引用计数很好理解:它为内存中的每个对象都维护一个引用计数器,每次对象被引用的时候,计数器就加一,对象被释放的时候,计数器就减一.

foo = 'xyz'                 # 此时计数是1
sys.getrefcount(foo)        # 此时会输出2,因为作为参数传递时,计数器会加一
bar = foo                   # 2
del foo                     # 1

但是,这种方法的缺点也很明显.

循环引用

循环引用使得一组对象的计数器永远不为0,及时他们的名字被删除了,但他们依然存留在内存中

a = []
a.append(a)     # 此时 a=[...]
del a

尽管我们这里 del 了 a ,但是我们只销毁了他的名字,它自己引用了自己,因此不能被引用计数器回收掉.

为了处理循环引用,python 又引入了 标记-清除 法作为引用计数的补充. 跟名字一样,这个算法确实使用了两个步骤来进行

也有算法对标记-回收法做了改进:

将内存分为1和2两大块,所有当前存在的对象的都存在内存1块上,然后标记时,将可达对象直接复制到内存2中,清除阶段直接清空内存1,这样就避免了内存碎片,并且过程也简单,但是代价比较昂贵

在标记-清除回收内存的时候,进程会被阻塞,一直到内存回收完成,因此也有性能问题.于是,就有了 分代回收 的方法,该方法基于这样一个统计:

分代回收是基于这样的一个统计事实,对于程序,存在一定比例的内存块的生存周期比较短; 而剩下的内存块,生存周期会比较长,甚至会从程序开始一直持续到程序结束。 这一比例通常在 80%~90% 之间,这种思想简单点说就是:对象存在时间越长,越可能不是垃圾,应该越少去收集。

gc 模块

import sys
import gc
gc.set_debug(gc.DEBUG_STATS|gc.DEBUG_LEAK)
a = ['aaaaaaa']
b = ['bbbbbbb']
a.append(a)
b.append(a)
sys.getrefcount(a)          # 3
del a
del b
gc.collect()                # 会打印出回收信息
gc.garbage                  # 会记录下回收的垃圾

上面调用 collect() 的时候,检测到 a 是一个不可达对象,将拆除自身循环,并且删除掉,这里 collect 会返回 gc: done, 2 unreachable, 0 uncollectable, 0.0070s elapsed. ,表示删除了两个不可达对象.如果我们只 del 的时候,只删除了a 或者 b,collect 是不会回收任何对象的

上面我们看到 collect 返回了0个 uncollectable ,说明所有不可达对象都被回收了.于是,也可能有不能被回收的情况:

import sys, gc
class A:
    def __init__(self, name):
        self.name = name
    def __del__(self):
        pass
class B:
    def __init__(self, name):
        self.name = name
    def __del__(self):
        pass
gc.set_debug(gc.DEBUG_STATS|gc.DEBUG_LEAK)
a = A('aaa')
b = B('bbb')
a.b = b
b.a = a
del a, b
gc.collect()
gc.garbageg

这个时候,我们能看到gc 模块无能为力了, uncollectable 不会是 0 ,因此就造成了内存泄露.

根据 python 的定义,对于有自己实现 __del__ 方法的对象,python 不会对它进行垃圾回收.因为 python 在资源释放之前,会先执行用户定义的 del 方法, 但是垃圾回收又没有保证按顺序回收,很有可能在 del b 之后才执行了 a 中的 del 方法,而该方法中又恰好使用了 b, 于是就会造成错误.

所以通常我们才会看到不要自定义 __del__ 方法(如果能保证没有循环引用,自定义也是可以的)

在默认情况下, gc.garbage 里面只会存放 uncolloctable 对象,而我们在 gc.set_debug() 中的设置,让所有 unreachable 但是可以被回收的对象也放进去了

弱引用

不太清除弱引用的实际工作过程

如上所述,在 Python 中主要通过引用计数来销毁对象,实际上我们可以通过 weakref 模块创建到对象的弱引用,这样 Python 会在对象的引用计数为 0 或只存在对象的弱引用时回收这个对象(当对象创建弱引用时,他的计数器并不会增加)。

import weakref
class A:
    pass
a = A()
print a             # <__main__.A instance at 0x10581c950>
b = weakref.ref(a)
print b             # <weakref at 0x10581e208; to 'instance' at 0x10581c950>
import weakref, gc
class A:
    def __del__(self):
        pass
a = A()
b = A()
a.b = weakref.ref(b)
b.a = weakref.ref(a)
del a, b
gc.collect()                # gc: done, 12 unreachable, 0 uncollectable, 0.0074s elapsed.



blog comments powered by Disqus