Python的垃圾回收机制

Posted petrolero

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python的垃圾回收机制相关的知识,希望对你有一定的参考价值。

Python的GC模块主要运用了“引用计数”(reference counting)来跟踪和回收垃圾。在引用计数的基础上,还可以通过“标记-清除”(mark and sweep)解决容器对象可能产生的循环引用的问题。通过“分代回收”(generation collection)以空间换取时间来进一步提高垃圾回收的效率。

引用计数机制:
    python里每一个东西都是对象,它们的核心就是一个结构体:PyObject
1 typedef struct_object {
2     int ob_refcnt;
3     struct_typeobject *ob_type;
4 }PyObject;

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

复制代码
1 #define Py_INCREF(op)   ((op)->ob_refcnt++)          //增加计数
2 #define Py_DECREF(op)      \\                         //减少计数        
3      if (--(op)->ob_refcnt != 0)    \\
4          ;        \\
5      else         \\
6          __Py_Dealloc((PyObject *)(op))
复制代码
引用计数为0时,该对象生命就结束了。
    引用计数机制的优点:
         1、简单
        2、实时性:一旦没有引用,内存就直接释放了。不用像其他机制等到特定时机。实时性还带来一个好处:处理回收内存的时间分摊到了平时。        
    引用计数机制的缺点: 
        1、维护引用计数消耗资源 
        2、循环引用 
 
1 list1 = []
2 list2 = []
3 list1.append(list2)
4 list2.append(list1)
list1与list2相互引用,如果不存在其他对象对它们的引用,list1与list2的引用计数也仍然为1,所占用的内存永远无法被回收,这将是致命的。
    对于如今的强大硬件,缺点1尚可接受,但是循环引用导致内存泄露,注定python还将引入新的回收机制。

上面说到python里回收机制是以引用计数为主,标记-清除和分代收集两种机制为辅。

1、标记-清除机制

标记-清除机制,顾名思义,首先标记对象(垃圾检测),然后清除垃圾(垃圾回收)。如图:

首先初始所有对象标记为白色,并确定根节点对象(这些对象是不会被删除),标记它们为黑色(表示对象有效)。将有效对象引用的对象标记为灰色(表示对象可达,但它们所引用的对象还没检查),检查完灰色对象引用的对象后,将灰色标记为黑色。重复直到不存在灰色节点为止。最后白色结点都是需要清除的对象。

2、回收对象的组织

这里所采用的高级机制作为引用计数的辅助机制,用于解决产生的循环引用问题。而循环引用只会出现在“内部存在可以对其他对象引用的对象”,比如:list,class等。

为了要将这些回收对象组织起来,需要建立一个链表。自然,每个被收集的对象内就需要多提供一些信息,下面代码是回收对象里必然出现的。

复制代码
1 /* GC information is stored BEFORE the object structure. */
2 typedef union _gc_head {
3     struct {
4         union _gc_head *gc_next;
5         union _gc_head *gc_prev;
6         Py_ssize_t gc_refs;
7     } gc;
8     long double dummy;  /* force worst-case alignment */
9 } PyGC_Head;
复制代码

一个对象的实际结构如图所示:

图2

通过PyGC_Head的指针将每个回收对象连接起来,形成了一个链表,也就是在1里提到的初始化的所有对象。

 

3、分代技术

分代技术是一种典型的以空间换时间的技术,这也正是java里的关键技术。这种思想简单点说就是:对象存在时间越长,越可能不是垃圾,应该越少去收集。

这样的思想,可以减少标记-清除机制所带来的额外操作。分代就是将回收对象分成数个代,每个代就是一个链表(集合),代进行标记-清除的时间与代内对象

存活时间成正比例关系

复制代码
 1 /*** Global GC state ***/
 2 
 3 struct gc_generation {
 4     PyGC_Head head;
 5     int threshold; /* collection threshold */
 6     int count; /* count of allocations or collections of younger
 7                   generations */
 8 };//每个代的结构
 9 
10 #define NUM_GENERATIONS 3//代的个数
11 #define GEN_HEAD(n) (&generations[n].head)
12 
13 /* linked lists of container objects */
14 static struct gc_generation generations[NUM_GENERATIONS] = {
15     /* PyGC_Head,                               threshold,      count */
16     {{{GEN_HEAD(0), GEN_HEAD(0), 0}},           700,            0},
17     {{{GEN_HEAD(1), GEN_HEAD(1), 0}},           10,             0},
18     {{{GEN_HEAD(2), GEN_HEAD(2), 0}},           10,             0},
19 };
20 
21 PyGC_Head *_PyGC_generation0 = GEN_HEAD(0);
复制代码

从上面代码可以看出python里一共有三代,每个代的threshold值表示该代最多容纳对象的个数。默认情况下,当0代超过700,或1,2代超过10,垃圾回收机制将触发。

0代触发将清理所有三代,1代触发会清理1,2代,2代触发后只会清理自己。

这篇算是一个完整的收集流程:链表建立,确定根节点,垃圾标记,垃圾回收~

1、链表建立

    首先,里在分代技术说过:0代触发将清理所有三代,1代触发会清理1,2代,2代触发后只会清理自己。在清理0代时,会将三个链表(代)链接起来,清理1代的时,会链接1,2两代。在后面三步,都是针对的这个建立之后的链表。

2、确定根节点

    图1为一个例子。list1与list2循环引用,list3与list4循环引用。a是一个外部引用。

 

对于这样一个链表,我们如何得出根节点呢。python里是在引用计数的基础上又提出一个有效引用计数的概念。顾名思义,有效引用计数就是去除循环引用后的计数。

下面是计算有效引用计数的相关代码:

复制代码
 1 /* Set all gc_refs = ob_refcnt.  After this, gc_refs is > 0 for all objects
 2  * in containers, and is GC_REACHABLE for all tracked gc objects not in
 3  * containers.
 4  */
 5 static void
 6 update_refs(PyGC_Head *containers)
 7 {
 8     PyGC_Head *gc = containers->gc.gc_next;
 9     for (; gc != containers; gc = gc->gc.gc_next) {
10         assert(gc->gc.gc_refs == GC_REACHABLE);
11         gc->gc.gc_refs = Py_REFCNT(FROM_GC(gc));
12         assert(gc->gc.gc_refs != 0);
13     }
14 }
15 
16 /* A traversal callback for subtract_refs. */
17 static int
18 visit_decref(PyObject *op, void *data)
19 {
20     assert(op != NULL);
21     if (PyObject_IS_GC(op)) {
22         PyGC_Head *gc = AS_GC(op);
23         /* We\'re only interested in gc_refs for objects in the
24          * generation being collected, which can be recognized
25          * because only they have positive gc_refs.
26          */
27         assert(gc->gc.gc_refs != 0); /* else refcount was too small */
28         if (gc->gc.gc_refs > 0)
29             gc->gc.gc_refs--;
30     }
31     return 0;
32 }
33 
34 /* Subtract internal references from gc_refs.  After this, gc_refs is >= 0
35  * for all objects in containers, and is GC_REACHABLE for all tracked gc
36  * objects not in containers.  The ones with gc_refs > 0 are directly
37  * reachable from outside containers, and so can\'t be collected.
38  */
39 static void
40 subtract_refs(PyGC_Head *containers)
41 {
42     traverseproc traverse;
43     PyGC_Head *gc = containers->gc.gc_next;
44     for (; gc != containers; gc=gc->gc.gc_next) {
45         traverse = Py_TYPE(FROM_GC(gc))->tp_traverse;
46         (void) traverse(FROM_GC(gc),
47                        (visitproc)visit_decref,
48                        NULL);
49     }
50 }
复制代码

update_refs函数里建立了一个引用的副本。

visit_decref函数对引用的副本减1,subtract_refs函数里traverse的作用是遍历对象里的每一个引用,执行visit_decref操作。

最后,链表内引用计数副本非0的对象,就是根节点了。

说明:

1、为什么要建立引用副本?

答:这个过程是寻找根节点的过程,在这个时候修改计数不合适。subtract_refs会对对象的引用对象执行visit_decref操作。如果链表内对象引用了链表外对象,那么链表外对象计数会减1,显然,很有可能这个对象会被回收,而回收机制里根本不应该对非回收对象处理。

2、traverse的疑问(未解决)?

答:一开始,有个疑问。上面例子里,subtract_refs函数中处理完list1结果应该如下:

 

然后gc指向list2,此时list2的副本(为0)不会减少,但是list2对list1还是存在实际上的引用,那么list1副本会减1吗?显然,如果减1就出问题了。

所以list1为0时,traverse根本不会再去处理list1这些引用(或者说,list2对list1名义上不存在引用了)。

此时,又有一个问题,如果存在一个外部对象b,对list2引用,subtract_refs函数中处理完list1后,如下图:

当subtract_refs函数中遍历到list2时,list2的副本还会减1吗?显然traverse的作用还是没有理解。

3、垃圾标记

   接下来,python建立两条链表,一条存放根节点,以及根节点的引用对象。另外一条存放unreachable对象。

标记的方法就是里的标记思路,代码如下:

复制代码
  1 /* A traversal callback for move_unreachable. */
  2 static int
  3 visit_reachable(PyObject *op, PyGC_Head *reachable)
  4 {
  5     if (PyObject_IS_GC(op)) {
  6         PyGC_Head *gc = AS_GC(op);
  7         const Py_ssize_t gc_refs = gc->gc.gc_refs;
  8 
  9         if (gc_refs == 0) {
 10             /* This is in move_unreachable\'s \'young\' list, but
 11              * the traversal hasn\'t yet gotten to it.  All
 12              * we need to do is tell move_unreachable that it\'s
 13              * reachable.
 14              */
 15             gc->gc.gc_refs = 1;
 16         }
 17         else if (gc_refs == GC_TENTATIVELY_UNREACHABLE) {
 18             /* This had gc_refs = 0 when move_unreachable got
 19              * to it, but turns out it\'s reachable after all.
 20              * Move it back to move_unreachable\'s \'young\' list,
 21              * and move_unreachable will eventually get to it
 22              * again.
 23              */
 24             gc_list_move(gc, reachable);
 25             gc->gc.gc_refs = 1;
 26         }
 27         /* Else there\'s nothing to do.
 28          * If gc_refs > 0, it must be in move_unreachable\'s \'young\'
 29          * list, and move_unreachable will eventually get to it.
 30          * If gc_refs == GC_REACHABLE, it\'s either in some other
 31          * generation so we don\'t care about it, or move_unreachable
 32          * already dealt with it.
 33          * If gc_refs == GC_UNTRACKED, it must be ignored.
 34          */
 35          else {
 36             assert(gc_refs > 0
 37                    || gc_refs == GC_REACHABLE
 38                    || gc_refs == GC_UNTRACKED);
 39          }
 40     }
 41     return 0;
 42 }
 43 
 44 /* Move the unreachable objects from young to unreachable.  After this,
 45  * all objects in young have gc_refs = GC_REACHABLE, and all objects in
 46  * unreachable have gc_refs = GC_TENTATIVELY_UNREACHABLE.  All tracked
 47  * gc objects not in young or unreachable still have gc_refs = GC_REACHABLE.
 48  * All objects in young after this are directly or indirectly reachable
 49  * from outside the original young; and all objects in unreachable are
 50  * not.
 51  */
 52 static void
 53 move_unreachable(PyGC_Head *young, PyGC_Head *unreachable)
 54 {
 55     PyGC_Head *gc = young->gc.gc_next;
 56 
 57     /* Invariants:  all objects "to the left" of us in young have gc_refs
 58      * = GC_REACHABLE, and are indeed reachable (directly or indirectly)
 59      * from outside the young list as it was at entry.  All other objects
 60      * from the original young "to the left" of us are in unreachable now,
 61      * and have gc_refs = GC_TENTATIVELY_UNREACHABLE.  All objects to the
 62      * left of us in \'young\' now have been scanned, and no objects here
 63      * or to the right have been scanned yet.
 64      */
 65 
 66     while (gc != young) {
 67         PyGC_Head *next;
 68 
 69         if (gc->gc.gc_refs) {
 70             /* gc is definitely reachable from outside the
 71              * original \'young\'.  Mark it as such, and traverse
 72              * its pointers to find any other objects that may
 73              * be directly reachable from it.  Note that the
 74              * call to tp_traverse may append objects to young,
 75              * so we have to wait until it returns to determine
 76              * the next object to visit.
 77              */
 78             PyObject *op = FROM_GC(gc);
 79             traverseproc traverse = Py_TYPE(op)->tp_traverse;
 80             assert(gc->gc.gc_refs > 0);
 81             gc->gc.gc_refs = GC_REACHABLE;
 82             (void) traverse(op,
 83                             (visitproc)visit_reachable,
 84                             (void *)young);
 85             next = gc->gc.gc_next;
 86         }
 87         else {
 88             /* This *may* be unreachable.  To make progress,
 89              * assume it is.  gc isn\'t directly reachable from
 90              * any object we\'ve already traversed, but may be
 91              * reachable from an object we haven\'t gotten to yet.
 92              * visit_reachable will eventually move gc back into
 93              * young if that\'s so, and we\'ll see it again. 
 94              */
 95             next = gc->gc.gc_next;
 96             gc_list_move(gc, unreachable);
 97             gc->gc.gc_refs = GC_TENTATIVELY_UNREACHABLE;
 98         }
 99         gc = next;
100     }
101 }
复制代码

标记之后,链表如上图。

4、垃圾回收

回收的过程,就是销毁不可达链表内对象。下面代码就是list的清除方法:

复制代码
 1 /* Methods */
 2 
 3 static void
 4 list_dealloc(PyListObject *op)
 5 {
 6     Py_ssize_t i;
 7     PyObject_GC_UnTrack(op);
 8     Py_TRASHCAN_SAFE_BEGIN(op)
 9     if (op->ob_item != NULL) {
10         /* Do it backwards, for Christian Tismer.
11            There\'s a simple test case where somehow this reduces
12            thrashing when a *very* large list is created and
13            immediately deleted. */
14         i = Py_SIZE(op);
15         while (--i >= 0) {
16             Py_XDECREF(op->ob_item[i]);
17         }
18         PyMem_FREE(op->ob_item);
19     }
20     if (numfree < PyList_MAXFREELIST && PyList_CheckExact(op))
21         free_list[numfree++] = op;
22     else
23         Py_TYPE(op)->tp_free((PyObject *)op);
24     Py_TRASHCAN_SAFE_END(op)
25 }

本文转自:https://www.cnblogs.com/hackerl/p/5901553.html

以上是关于Python的垃圾回收机制的主要内容,如果未能解决你的问题,请参考以下文章

Python 垃圾回收机制

一文读懂Python垃圾回收机制收藏版

一文读懂Python垃圾回收机制收藏版

一文读懂Python垃圾回收机制收藏版

一文读懂Python垃圾回收机制收藏版

一文读懂Python垃圾回收机制收藏版