zoukankan      html  css  js  c++  java
  • 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
  • 相关阅读:
    单例模式
    Curator Zookeeper分布式锁
    LruCache算法原理及实现
    lombok 简化java代码注解
    Oracle客户端工具出现“Cannot access NLS data files or invalid environment specified”错误的解决办法
    解决mysql Table ‘xxx’ is marked as crashed and should be repaired的问题。
    Redis 3.0 Cluster集群配置
    分布式锁的三种实现方式
    maven发布项目到私服-snapshot快照库和release发布库的区别和作用及maven常用命令
    How to Use Convolutional Neural Networks for Time Series Classification
  • 原文地址:https://www.cnblogs.com/petrolero/p/9787105.html
Copyright © 2011-2022 走看看