python中的垃圾回收机制

概述

现在的高级语言如java,c#等,都采用了垃圾收集机制,而不再是c,c++里用户自己管理维护内存的方式。自己管理内存极其自由,可以任意申请内存,但如同一把双刃剑,为大量内存泄露,悬空指针等bug埋下隐患。
对于一个字符串、列表、类甚至数值都是对象,且定位简单易用的语言,自然不会让用户去处理如何分配回收内存的问题。
python里也同java一样采用了垃圾收集机制,不过不一样的是:
python采用的是引用计数机制为主,标记-清除分代收集两种机制为辅的策略。

引用计数机制

python里每一个东西都是对象,它们的核心就是一个结构体:PyObject

typedef struct_object {
 int ob_refcnt;
 struct_typeobject *ob_type;
} PyObject;

PyObject是每个对象必有的内容,其中ob_refcnt就是做为引用计数,用来记录对象被引用的次数。当一个对象有新的引用时,它的ob_refcnt就会增加,当引用被删除,它的ob_refcnt就会减少

#define Py_INCREF(op)   ((op)->ob_refcnt++) //增加计数
#define Py_DECREF(op) \ //减少计数
    if (--(op)->ob_refcnt != 0) \
        ; \
    else \
        __Py_Dealloc((PyObject *)(op))

当引用计数为0时,该对象生命就结束了,就会被当做垃圾进行清理,并回收内存空间。

引用计数机制的优点:

  1. 简单高效,运行期没有停顿;
  2. 实时性:一旦没有引用,内存就直接释放了,不用像其他机制等到特定时机。实时性还带来一个好处:处理回收内存的时间分摊到了平时。

引用计数机制的缺点:

  1. 维护引用计数消耗一定的资源;
  2. 无法解决循环引用问题。
list1 = ['a']
list2 = ['b']
list1.append(list2)
list2.append(list1)

list1与list2相互引用,如果不存在其他对象对它们的引用,list1与list2的引用计数也仍然为1,所占用的内存永远无法被回收,这将是致命的。
对于如今的强大硬件,缺点1尚可接受,但是循环引用导致内存泄露,注定python还将引入新的回收机制。(标记清除和分代收集)

python中的对象分配 

如下是一个简单类

 

 

 

 

 

 

 

我们用Pyhon来创建一个Node对象,当创建对象时Python立即向操作系统请求内存:

当我们创建第二个对象的时候,Python再次向操作系统请求内存:

在内部,创建一个对象时,Python总是在对象的结构体PyObject里保存一个整数,称为引用数

期初,Python将这个值设置为1,如下三个Node对象:

值为1说明分别有一个指针指向或引用这三个对象,假如我们现在创建一个新的Node实例,JKL,并用n1指向它:

与之前一样,Python设置JKL的引用数为1。然而,请注意由于我们改变了n1指向了JKL,不再指向ABC,Python就把ABC的引用数置为0了。
此刻,Python垃圾回收器立刻挺身而出!每当对象的引用数减为0,Python立即将其释放,把内存还给操作系统:

上面Python回收了ABC Node实例使用的内存,这种垃圾回收算法被称为引用计数

就好像有个患有轻度OCD(一种强迫症)的室友一刻不停地跟在你身后打扫,你一放下脏碟子或杯子,有个家伙已经准备好把它放进洗碗机了!

现在,假如我们让n2引用n1:

 上图中左边的DEF的引用数已经被Python减少了,垃圾回收器会立即回收DEF实例。同时JKL的引用数已经变为了2 ,因为n1和n2都指向它。

标记-清除

这种方式主要用在Ruby里,即当预创建对象都被程序用过了,可用列表里已经空空如也了。此时,首先Ruby把程序停下来,采用"地球停转垃圾回收大法",轮询所有指针,变量和代码产生别的引用对象和其他值,同时Ruby通过自身的虚拟机遍历内部指针,标记出这些指针引用的每个对象。

如果说被标记的对象是存活的,剩下的未被标记的对象只能是垃圾,这意味着我们的代码不再会使用它了。接下来Ruby清除这些无用的垃圾对象,把它们送回到可用列表中,现在等到下回再创建对象的时候Ruby又可以把这些垃圾对象(已经可用)分给我们使用了。

  在python中,这种方式主要用来处理循环引用的情况。

如果一个数据结构引用了它自身,即如果这个数据结构是一个循环数据结构,那么某些引用计数值是肯定无法变成零的。

举个例子,定义节点类并创建两个节点,ABC以及DEF,两个节点的引用计数都被初始化为1,因为各有两个引用指向各个节点(n1和n2)。

 现在,让我们在节点中定义两个附加的属性,next以及prev,我们设置 n1.next 指向 n2,同时设置 n2.prev 指回 n1。

现在,假定我们的程序不再使用这两个节点了,我们将 n1 和 n2 都设置为None,Python会将每个节点的引用计数减少到1。

 

请注意在以上刚刚说到的例子中,我们以一个不是很常见的情况结尾:我们有一个“孤岛”或是一组未使用的、互相指向的对象,但是谁都没有外部引用。

换句话说,我们的程序不再使用这些节点对象了,所以我们希望Python的垃圾回收机制能够足够智能去释放这些对象并回收它们占用的内存空间。

但是这不可能,因为所有的引用计数都是1而不是0,Python的引用计数算法不能够处理互相指向自己的对象。

  这就是为什么Python要引入Generational GC算法的原因!正如Ruby使用一个链表(free list)来持续追踪未使用的、自由的对象一样,Python使用一种不同的链表来持续追踪活跃的对象。而不将其称之为“活跃列表”,Python的内部C代码将其称为零代(Generation Zero)。每次当你创建一个对象或其他什么值的时候,Python会将其加入零代链表:

 从上边可以看到当我们创建ABC节点的时候,Python将其加入零代链表。请注意到这并不是一个真正的列表,并不能直接在你的代码中访问,事实上这个链

表是一个完全内部的Python运行时,相似的,当我们创建DEF节点的时候,Python将其加入同样的链表:

现在零代包含了两个节点对象。

随后,Python会循环遍历零代列表上的每个对象,检查列表中每个互相引用的对象,根据规则减掉其引用计数。在这个过程中,Python会一个接一个的统计内部引用的数量以防过早地释放对象。

通过识别内部引用,Python能够减少许多零代链表对象的引用计数。在上图的第一行中你能够看见ABC和DEF的引用计数已经变为零了,这意味着收集器可以释放它们并回收内存空间了。剩下的活跃的对象则被移动到一个新的链表:一代链表。

从某种意义上说,Python的GC算法类似于Ruby所用的标记回收算法。周期性地从一个对象到另一个对象追踪引用以确定对象是否还是活跃的,正在被程序所使用的,这正类似于Ruby的标记过程。

小结

乍眼一看,Ruby和Python的GC实现是截然不同的,Ruby使用John-MaCarthy的原生“标记并清除”算法,而Python使用引用计数。但是仔细看来,可以发现Python使用了些许标记清除的思想来处理循环引用,而两者同时以相似的方式使用基于代的垃圾回收算法。

总体来说,在Python中,主要通过引用计数进行垃圾回收;通过 “标记-清除” 解决容器对象可能产生的循环引用问题;通过 “分代回收” 以空间换时间的方法提高垃圾回收效率。

posted on 2021-01-19 16:45  流年似水zlw  阅读(152)  评论(0)    收藏  举报

导航