python垃圾回收機制


轉載自: http://my.oschina.net/hebianxizao/blog/57367

現在的高級語言如java,c#等,都采用了垃圾收集機制,而不再是c,c++里用戶自己管理維護內存的方式。自己管理內存極其自由,可以任意申請內存,但如同一把雙刃劍,為大量內存泄露,懸空指針等bug埋下隱患。     

    對於一個字符串、列表、類甚至數值都是對象,且定位簡單易用的語言,自然不會讓用戶去處理如何分配回收內存的問題。
    python里也同java一樣采用了垃圾收集機制,不過不一樣的是,python采用的是引用計數機制為主,標記-清除和分代收集兩種機制為輔的策略。
    引用計數機制:
    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、標記-清除機制

標記-清除機制,顧名思義,首先標記對象(垃圾檢測),然后清除垃圾(垃圾回收)。如圖1:

image

                            圖1

首先初始所有對象標記為白色,並確定根節點對象(這些對象是不會被刪除),標記它們為黑色(表示對象有效)。將有效對象引用的對象標記為灰色(表示對象可達,

但它們所引用的對象還沒檢查),檢查完灰色對象引用的對象后,將灰色標記為黑色。重復直到不存在灰色節點為止。最后白色結點都是需要清除的對象。

2、回收對象的組織

這里所采用的高級機制作為引用計數的輔助機制,用於解決產生的循環引用問題。而循環引用只會出現在“內部存在可以對其他對象引用的對象”,比如:list,class等。

為了要將這些回收對象組織起來,需要建立一個鏈表。自然,每個被收集的對象內就需要多提供一些信息,下面代碼是回收對象里必然出現的。

一個對象的實際結構如圖2:

                  image

                           圖2

通過PyGC_Head的指針將每個回收對象連接起來,形成了一個鏈表,也就是在1里提到的初始化的所有對象。

3、分代技術

分代技術是一種典型的以空間換時間的技術,這也正是java里的關鍵技術。這種思想簡單點說就是:對象存在時間越長,越可能不是垃圾,應該越少去收集。

這樣的思想,可以減少標記-清除機制所帶來的額外操作。分代就是將回收對象分成數個代,每個代就是一個鏈表(集合),代進行標記-清除的時間與代內對象

存活時間成正比例關系。

從上面代碼可以看出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是一個外部引用。

image

                                     圖1

對於這樣一個鏈表,我們如何得出根節點呢。python里是在引用計數的基礎上又提出一個有效引用計數的概念。顧名思義,有效引用計數就是去除循環引用后的計數。

下面是計算有效引用計數的相關代碼:

01 /* Set all gc_refs = ob_refcnt.  After this, gc_refs is > 0 for all objects
02  * in containers, and is GC_REACHABLE for all tracked gc objects not in
03  * containers.
04  */
05 static void
06 update_refs(PyGC_Head *containers)
07 {
08     PyGC_Head *gc = containers->gc.gc_next;
09     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結果應該如下:

image

然后gc指向list2,此時list2的副本(為0)不會減少,但是list2對list1還是存在實際上的引用,那么list1副本會減1嗎?顯然,如果減1就出問題了。

所以list1為0時,traverse根本不會再去處理list1這些引用(或者說,list2對list1名義上不存在引用了)。

此時,又有一個問題,如果存在一個外部對象b,對list2引用,subtract_refs函數中處理完list1后,如下圖:

image

當subtract_refs函數中遍歷到list2時,list2的副本還會減1嗎?顯然traverse的作用還是沒有理解。

3、垃圾標記

   接下來,python建立兩條鏈表,一條存放根節點,以及根節點的引用對象。另外一條存放unreachable對象。

標記的方法就是里的標記思路,代碼如下:

001 /* A traversal callback for move_unreachable. */
002 static int
003 visit_reachable(PyObject *op, PyGC_Head *reachable)
004 {
005     if (PyObject_IS_GC(op)) {
006         PyGC_Head *gc = AS_GC(op);
007         const Py_ssize_t gc_refs = gc->gc.gc_refs;
008  
009         if (gc_refs == 0) {
010             /* This is in move_unreachable's 'young' list, but
011              * the traversal hasn't yet gotten to it.  All
012              * we need to do is tell move_unreachable that it's
013              * reachable.
014              */
015             gc->gc.gc_refs = 1;
016         }
017         else if (gc_refs == GC_TENTATIVELY_UNREACHABLE) {
018             /* This had gc_refs = 0 when move_unreachable got
019              * to it, but turns out it's reachable after all.
020              * Move it back to move_unreachable's 'young' list,
021              * and move_unreachable will eventually get to it
022              * again.
023              */
024             gc_list_move(gc, reachable);
025             gc->gc.gc_refs = 1;
026         }
027         /* Else there's nothing to do.
028          * If gc_refs > 0, it must be in move_unreachable's 'young'
029          * list, and move_unreachable will eventually get to it.
030          * If gc_refs == GC_REACHABLE, it's either in some other
031          * generation so we don't care about it, or move_unreachable
032          * already dealt with it.
033          * If gc_refs == GC_UNTRACKED, it must be ignored.
034          */
035          else {
036             assert(gc_refs > 0
037                    || gc_refs == GC_REACHABLE
038                    || gc_refs == GC_UNTRACKED);
039          }
040     }
041     return 0;
042 }
043  
044 /* Move the unreachable objects from young to unreachable.  After this,
045  * all objects in young have gc_refs = GC_REACHABLE, and all objects in
046  * unreachable have gc_refs = GC_TENTATIVELY_UNREACHABLE.  All tracked
047  * gc objects not in young or unreachable still have gc_refs = GC_REACHABLE.
048  * All objects in young after this are directly or indirectly reachable
049  * from outside the original young; and all objects in unreachable are
050  * not.
051  */
052 static void
053 move_unreachable(PyGC_Head *young, PyGC_Head *unreachable)
054 {
055     PyGC_Head *gc = young->gc.gc_next;
056  
057     /* Invariants:  all objects "to the left" of us in young have gc_refs
058      * = GC_REACHABLE, and are indeed reachable (directly or indirectly)
059      * from outside the young list as it was at entry.  All other objects
060      * from the original young "to the left" of us are in unreachable now,
061      * and have gc_refs = GC_TENTATIVELY_UNREACHABLE.  All objects to the
062      * left of us in 'young' now have been scanned, and no objects here
063      * or to the right have been scanned yet.
064      */
065  
066     while (gc != young) {
067         PyGC_Head *next;
068  
069         if (gc->gc.gc_refs) {
070             /* gc is definitely reachable from outside the
071              * original 'young'.  Mark it as such, and traverse
072              * its pointers to find any other objects that may
073              * be directly reachable from it.  Note that the
074              * call to tp_traverse may append objects to young,
075              * so we have to wait until it returns to determine
076              * the next object to visit.
077              */
078             PyObject *op = FROM_GC(gc);
079             traverseproc traverse = Py_TYPE(op)->tp_traverse;
080             assert(gc->gc.gc_refs > 0);
081             gc->gc.gc_refs = GC_REACHABLE;
082             (void) traverse(op,
083                             (visitproc)visit_reachable,
084                             (void *)young);
085             next = gc->gc.gc_next;
086         }
087         else {
088             /* This *may* be unreachable.  To make progress,
089              * assume it is.  gc isn't directly reachable from
090              * any object we've already traversed, but may be
091              * reachable from an object we haven't gotten to yet.
092              * visit_reachable will eventually move gc back into
093              * young if that's so, and we'll see it again.
094              */
095             next = gc->gc.gc_next;
096             gc_list_move(gc, unreachable);
097             gc->gc.gc_refs = GC_TENTATIVELY_UNREACHABLE;
098         }
099         gc = next;
100     }
101 }

image

標記之后,鏈表如上圖。

4、垃圾回收

回收的過程,就是銷毀不可達鏈表內對象。下面代碼就是list的清除方法:

01 /* Methods */
02  
03 static void
04 list_dealloc(PyListObject *op)
05 {
06     Py_ssize_t i;
07     PyObject_GC_UnTrack(op);
08     Py_TRASHCAN_SAFE_BEGIN(op)
09     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 }


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM