Python垃圾回收与内存泄露

    Python是面向对象、高级编程语言,其世界里万物皆对象,当我们编写的程序运行时,代码中定义的对象在物理内存中会占用相应的空间。现在流行的高级语言如Java,C#等都采用了垃圾收集机制自动管理内存使用,而不像C,C++需要用户自己分配、释放内存。自己管理内存的优点是自由灵活,可以任意申请内存,但存在致命的缺点是可能会造成内存泄露。

    Python解释器内核采用内存池方式管理物理内存,当创建新对象时,解释器在预先申请的物理内存块上分配相应的空间给对象使用,这样可以避免频繁的分配和释放物理内存。那么这些内存在什么时候释放呢?这涉及到Python对象的引用计数和垃圾回收。

1. 相关概念

1.1 什么是垃圾

    先看一个例子。

# -*- coding: utf8 -*-

class A(object):
    def __init__(self):
        self.data = [x for x in range(10000)]
        self.child = None
       
def ref():
    a1 = A()
    a2 = A()
    
    a1.child = a2

    在上述代码中,定义了类A,以及ref函数。在ref函数中,申明了A的两个实例对象,并且变量a1、a2分别指向这两个对象,且a1引用了a2指向的对象。当ref函数结束后,也就是a1和a2离开了作用域,在python解释器内部无任何地方引用这两个对象,因此a1、a2指向的两个对象变成“垃圾”对象。这些对象也就是所谓的内存垃圾,python解释器有一套垃圾回收机制,确保内存中无用对象及其空间及时被清理。   

1.2 什么是垃圾回收

    Python垃圾回收是指内存不再使用时的释放和回收过程。Python通过两种机制实现垃圾回收:引用计数能解决循环引用问题的垃圾收集器。

garbage collection

    The process of freeing memory when it is not used anymore. Python performs garbage collection via reference counting and a cyclic garbage collector that is able to detect and break reference cycles.

                                                                                                                                                  —— from python doc

1.2.1 引用计数

    引用计数是每个python对象的一个属性,该属性记录着有多少变量引用(指向)了该对象,该属性就称之为引用计数。将一个对象直接或者间接赋值给一个变量时,对象的计数器会加1 ;当变量被del删除,或者离开变量所在作用域时,对象的引用计数器会减 1。当引用计数归零时,代表无任何地方引用该对象,解释器将该对象安全的销毁。我们可以通过sys模块getrefcount()函数获取对象当前的引用计数。

1.2.2 垃圾收集器

    引用计数存在一个比较严重的缺陷是,无法及时回收存在循环引用对象。只有容器对象才会形成循环引用,比如list、class、deque、dict、set等都属于容器类型,那么什么是循环引用?看下下面这个例子:

# -*- coding: utf8 -*-

class A(object):
    def __init__(self):
        self.data = [x for x in range(10000)]
        self.child = None
        
def cycle_ref():
    a1 = A()
    a2 = A()

    a1.child = a2
    a2.child = a1

    上述代码cycle_ref函数,a1、a2指向的对象即存在循环引用。循环引用即两个对象互相引用对方,循环引用可能带来内存泄露问题。当函数cycle_ref结束后,a1、a2离开其作用域,因此他们指向的对象的引用计数减 1。但由于互相引用,两个对象的引用计数始终为 1,因此解释器不会对其进行垃圾回收,从而可能造成内存泄露。比如运行下面的粗暴代码,可以非常明显地看到内存一直在飙升:

# -*- coding: utf8 -*-

class A(object):
    def __init__(self):
        self.data = [x for x in range(100000)]
        self.child = None

    def __del__(self):
        pass

def cycle_ref():
    a1 = A()
    a2 = A()

    a1.child = a2
    a2.child = a1

if __name__ == '__main__':
    import time
    while True:
        time.sleep(0.5)
        cycle_ref()

    对于循环引用带来的问题,python解释器提供了垃圾收集器(gc)模块,gc使用分代回收算法回收垃圾。

    所谓分代回收,是一种以空间换时间的操作方式。Python将内存根据对象的存活时间划分为不同的集合,每个集合称为一个代,Python将内存分为了三个generation(代),分别为年轻代(第 0 代)、中年代(第 1 代)、老年代(第 2 代),他们对应的是3个链表,它们的垃圾收集频率与对象的存活时间的增大而减小。新创建的对象都会分配在第 0 代,年轻代链表的总数达到设定阈值时,Python垃圾收集机制就会被触发,把那些可以被回收的对象回收掉,而那些不会回收的对象就会被移到中年代去,依此类推。老年代中的对象是存活时间最久的对象,甚至是存活于整个系统的生命周期内。同时,分代回收是建立在标记清除技术基础之上。分代回收同样作为Python的辅助垃圾收集技术处理那些容器对象。

    分代回收在实现上,支持垃圾收集的对象(主要是容器对象),其内核的PyTypeObject结构体对象的tp_flags变量的Py_TYFLAGS_HAVE_GC位为1。凡是该标记位为1的对象,其底层物理内存的分配使用_PyObject_GC_Malloc函数,其他使用PyObject_Malloc函数。_PyObject_GC_Malloc本质上也是调用PyObject_Malloc函数在内存池上分配内存,但是会多分配PyGC_Head结构体大小的内存,该PyGC_Head位于对象实际内存的前面。PyGC_Head有一个gc_refs属性,垃圾收集器通过判断gc_refs值来实现垃圾回收。

    所有支持垃圾收集的对象,在创建时都会被添加到一个gc双向链表,也就是前面所说的第 0 代的链表头部(解释器c源码中的_PyGC_generation0)。另外还有两个gc双向链表,存储了第 1 代第 2 代对象。垃圾收集主要流程如下:

1. 对于每一个容器对象,设置一个gc_refs值,并将其初始化为该对象的引用计数值。

2. 对于每一个容器对象,找到所有其引用的对象,将被引用对象的gc_refs值减1。

3. 执行完步骤2以后所有gc_refs值还大于0的对象都被非容器对象引用着,至少存在一个非循环引用。因此不能释放这些对象,将他们放入另一个集合。

4. 在步骤3中不能被释放的对象,如果他们引用着某个对象,被引用的对象也是不能被释放的, 因此将这些对象也放入另一个集合中。

5. 此时还剩下的对象都是无法到达(unreachable)的对象, 现在可以释放这些对象了。

    在循环引用中,对于unreachable、但collectable的对象,Python的gc垃圾回收机制能够定时自动回收这些对象。但是如果对象定义了__del__方法,这些对象变为uncollectable,垃圾回收机制无法收集这些对象,这也就是上面代码发生内存泄露的原因。

    Python解释器标准库对外暴露的gc模块,提供了对内部垃圾收集的访问及配置等接口,比如开启或关闭gc、设置回收阈值、获取对象的引用对象等。在需要的地方,我们可以手动执行垃圾回收,及时清理不必要的内存对象。

2. 解决内存泄露

    到这里,已经明确了如果存在循环引用,并且被循环引用的对象定义了__del__方法,就会发生内存泄露。如果我们的代码无法避免循环引用,但只要没有定义__del__方法,并且保证gc模块被打开,就不会发生内存泄露。

    但是由于gc垃圾收集机制,要遍历所有被垃圾收集器管理的python对象(包括垃圾和非垃圾对象),该过程比较耗时可能会造成程序卡顿,会对某些对内存、cpu要求较高的场景造成性能影响。那怎么才能优雅地避免内存泄露呢?

2.1 编写安全的代码

    前面提到过,如果被循环引用的对象未定义__del__方法,就不会发生内存泄露,因为解释器的gc机制确保了垃圾对象的定时回收。如果被循环引用的对象定义了__del__方法,但是只要编写足够安全的代码,也可以保证不发生内存泄露。比如对于上面发生内存泄露的cycle_ref函数,在函数结束前解除循环引用,即可解决内存泄露问题。

def cycle_ref():
    a1 = A()
    a2 = A()

    a1.child = a2
    a2.child = a1

    # 解除循环引用,避免内存泄露
    a1.child  = None
    a2.child  = None

   对于上述方法,我们有可能会忘记那一两行无关紧要的代码而造成灾难性后果,毕竟老虎也有打盹的时候。那怎么办?不要着急,Python已经为我们考虑到这点:弱引用。

2.2 弱引用

    Python标准库提供了weakref模块,弱引用不会在引用计数中计数,其主要目的是解决循环引用。并非所有的对象都支持weakref,例如list和dict就不支持。下面是weakref比较常用的方法:

1. class weakref.ref(object[, callback]) :创建一个弱引用对象,object是被引用的对象,callback是回调函数(当被引用对象被删除时,调用该回调函数)

2.weakref.proxy(object[, callback]):创建一个用弱引用实现的代理对象,参数同上

3.weakref.getweakrefcount(object) :获取对象object关联的弱引用对象数

4.weakref.getweakrefs(object):获取object关联的弱引用对象列表

5.class weakref.WeakKeyDictionary([dict]):创建key为弱引用对象的字典

6.class weakref.WeakValueDictionary([dict]):创建value为弱引用对象的字典

7.class weakref.WeakSet([elements]):创建成员为弱引用对象的集合对象

    同样对于上面发生内存泄露的cycle_ref函数,使用weakref稍加改造,便可更安全地解决内存泄露问题:

# -*- coding: utf8 -*-
import weakref

class A(object):
    def __init__(self):
        self.data = [x for x in range(100000)]
        self.child = None

    def __del__(self):
        pass

def cycle_ref():
    a1 = A()
    a2 = A()

    a1.child = weakref.proxy(a2)
    a2.child = weakref.proxy(a1)

if __name__ == '__main__':
    import time
    while True:
        time.sleep(0.5)
        cycle_ref()

 

   

  • 23
    点赞
  • 80
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

兔子要咬手指

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值