最近想了解一下Python的內存回收機制,特此來標記一下
平時在寫代碼的時候,關注的是寫出能實現業務邏輯的代碼,因為現在計算機的內存也比較寬裕,所以寫程序的時候也就沒怎么考慮垃圾回收這一方面的知識。俗話說,出來混總是要還的,所以既然每次都伸手向內存索取它的資源,那么還是需要知道什么時候以及如何把它還回去比較好。嘻嘻。
我們從三個方面來了解一下Python的垃圾回收機制。
一、引用計數
Python垃圾回收主要以引用計數為主,分代回收為輔。引用計數法的原理是每個對象維護一個ob_ref,用來記錄當前對象被引用的次數,也就是來追蹤到底有多少引用指向了這個對象,當發生以下四種情況的時候,該對象的引用計數器+1
- 對象被創建 a=14
- 對象被引用 b=a
- 對象被作為參數,傳到函數中 func(a)
- 對象作為一個元素,存儲在容器中 List={a,”a”,”b”,2}
與上述情況相對應,當發生以下四種情況時,該對象的引用計數器-1
- 當該對象的別名被顯式銷毀時 del a
- 當該對象的引別名被賦予新的對象, a=26
- 一個對象離開它的作用域,例如 func函數執行完畢時,函數里面的局部變量的引用計數器就會減一(但是全局變量不會)
將該元素從容器中刪除時,或者容器被銷毀時。
.當指向該對象的內存的引用計數器為0的時候,該內存將會被Python虛擬機銷毀
下面來補充一下它的源碼分析:
Python里面每一個東西都是對象,他們的核心是一個結構體Py_Object,所有Python對象的頭部包含了這樣一個結構PyObject
// object.h
struct _object {
Py_ssize_t ob_refcnt; # 引用計數值
struct PyTypeObject *ob_type;
} PyObject;
- 1
- 2
- 3
- 4
- 5
看一個比較具體點的例子,int型對象的定義:
// intobject.h
typedef struct {
PyObject_HEAD
long ob_ival;
} PyIntObject;
- 1
- 2
- 3
- 4
- 5
簡而言之,PyObject是每個對象必有的內容,其中ob_refcnt就是做為引用計數。當一個對象有新的引用時,它的ob_refcnt就會增加,當引用它的對象被刪除,它的ob_refcnt就會減少。當引用計數為0時,該對象生命就結束了。
#define Py_INCREF(op) ((op)->ob_refcnt++) //增加計數
#define Py_DECREF(op) \ //減少計數
if (--(op)->ob_refcnt != 0) \
; \
else \
__Py_Dealloc((PyObject *)(op))
- 1
- 2
- 3
- 4
- 5
- 6
引用計數法有很明顯的優點:
- 高效
- 運行期沒有停頓 可以類比一下Ruby的垃圾回收機制,也就是 實時性:一旦沒有引用,內存就直接釋放了。不用像其他機制等到特定時機。實時性還帶來一個好處:處理回收內存的時間分攤到了平時。
- 對象有確定的生命周期
- 易於實現
原始的引用計數法也有明顯的缺點:
- 維護引用計數消耗資源,維護引用計數的次數和引用賦值成正比,而不像mark and sweep等基本與回收的內存數量有關。
- 無法解決循環引用的問題。A和B相互引用而再沒有外部引用A與B中的任何一個,它們的引用計數都為1,但顯然應該被回收。
循環引用的示例:
list1 = []
list2 = []
list1.append(list2)
list2.append(list1)
- 1
- 2
- 3
- 4
為了解決這兩個致命弱點,Python又引入了以下兩種GC機制。
二、標記-清除
針對循環引用的情況:我們有一個“孤島”或是一組未使用的、互相指向的對象,但是誰都沒有外部引用。換句話說,我們的程序不再使用這些節點對象了,所以我們希望Python的垃圾回收機制能夠足夠智能去釋放這些對象並回收它們占用的內存空間。但是這不可能,因為所有的引用計數都是1而不是0。Python的引用計數算法不能夠處理互相指向自己的對象。你的代碼也許會在不經意間包含循環引用並且你並未意識到。事實上,當你的Python程序運行的時候它將會建立一定數量的“浮點數垃圾”,Python的GC不能夠處理未使用的對象因為應用計數值不會到零。
這就是為什么Python要引入Generational GC算法的原因!
注:
『標記清除(Mark—Sweep)』算法是一種基於追蹤回收(tracing GC)技術實現的垃圾回收算法。它分為兩個階段:第一階段是標記階段,GC會把所有的『活動對象』打上標記,第二階段是把那些沒有標記的對象『非活動對象』進行回收。那么GC又是如何判斷哪些是活動對象哪些是非活動對象的呢?
對象之間通過引用(指針)連在一起,構成一個有向圖,對象構成這個有向圖的節點,而引用關系構成這個有向圖的邊。從根對象(root object)出發,沿着有向邊遍歷對象,可達的(reachable)對象標記為活動對象,不可達的對象就是要被清除的非活動對象。根對象就是全局變量、調用棧、寄存器。
在上圖中,我們把小黑圈視為全局變量,也就是把它作為root object,從小黑圈出發,對象1可直達,那么它將被標記,對象2、3可間接到達也會被標記,而4和5不可達,那么1、2、3就是活動對象,4和5是非活動對象會被GC回收。
標記清除算法作為Python的輔助垃圾收集技術主要處理的是一些容器對象,比如list、dict、tuple,instance等,因為對於字符串、數值對象是不可能造成循環引用問題。Python使用一個雙向鏈表將這些容器對象組織起來。不過,這種簡單粗暴的標記清除算法也有明顯的缺點:清除非活動的對象前它必須順序掃描整個堆內存,哪怕只剩下小部分活動對象也要掃描所有對象。
正如Ruby使用一個鏈表(free list)來持續追蹤未使用的、自由的對象一樣,Python使用一種不同的鏈表來持續追蹤活躍的對象。而不將其稱之為“活躍列表”,Python的內部C代碼將其稱為零代(Generation Zero)。每次當你創建一個對象或其他什么值的時候,Python會將其加入零代鏈表:
“標記-清除”法是為了解決循環引用問題。可以包含其他對象引用的容器對象(如list, dict, set,甚至class)都可能產生循環引用,為此,在申請內存時,所有容器對象的頭部又加上了PyGC_Head來實現“標記-清除”機制。任何一個python對象都分為兩部分: PyObject_HEAD + 對象本身數據
// objimpl.h
typedef union _gc_head {
struct {
union _gc_head *gc_next;
union _gc_head *gc_prev;
Py_ssize_t gc_refs;
} gc;
long double dummy; /* force worst-case alignment */
} PyGC_Head;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
在為對象申請內存的時候,可以明顯看到,實際申請的內存數量已經加上了PyGC_Head的大小
// gcmodule.c
PyObject *
_PyObject_GC_Malloc(size_t basicsize)
{
PyObject *op;
PyGC_Head *g = (PyGC_Head *)PyObject_MALLOC(
sizeof(PyGC_Head) + basicsize); # 注意這里的sizeof(PyGC_Head)
if (g == NULL)
return PyErr_NoMemory();
......
op = FROM_GC(g);
return op;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
舉例來說,從list對象的創建中,有如下主要邏輯:
// listobject.c
PyObject *
PyList_New(Py_ssize_t size)
{
PyListObject *op;
......
op = PyObject_GC_New(PyListObject, &PyList_Type);
......
_PyObject_GC_TRACK(op); # _PyObject_GC_TRACK就將對象鏈接到了第0代對象集合中
return (PyObject *) op;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
每次當你創建一個對象或其他什么值的時候,Python會將其加入零代鏈表,示意圖如下:(圖中的prev和next就是PyGC_Head中的union _gc_head *gc_next;union _gc_head *gc_prev)
我們創建ABC節點的時候,Python將其加入零代鏈表。請注意到這並不是一個真正的列表,並不能直接在你的代碼中訪問,事實上這個鏈表是一個完全內部的Python運行時。
相似的,當我們創建DEF節點的時候,Python將其加入同樣的鏈表:
現在零代包含了兩個節點對象。(他還將包含Python創建的每個其他值,與一些Python自己使用的內部值。)
檢測循環引用
隨后,Python會循環遍歷零代列表上的每個對象,檢查列表中每個互相引用的對象,根據規則減掉其引用計數。在這個過程中,Python會一個接一個的統計內部引用的數量以防過早地釋放對象。
為了便於理解,來看一個例子:
從上面可以看到 ABC 和 DEF 節點包含的引用數為1.有三個其他的對象同時存在於零代鏈表中,藍色的箭頭指示了有一些對象正在被零代鏈表之外的其他對象所引用。(接下來我們會看到,Python中同時存在另外兩個分別被稱為一代和二代的鏈表)。這些對象有着更高的引用計數因為它們正在被其他指針所指向着。
接下來你會看到Python的GC是如何處理零代鏈表的。
通過識別內部引用,Python能夠減少許多零代鏈表對象的引用計數。在上圖的第一行中你能夠看見ABC和DEF的引用計數已經變為零了,這意味着收集器可以釋放它們並回收內存空間了。剩下的活躍的對象則被移動到一個新的鏈表:一代鏈表。
從某種意義上說,Python的GC算法類似於Ruby所用的標記回收算法。周期性地從一個對象到另一個對象追蹤引用以確定對象是否還是活躍的,正在被程序所使用的,這正類似於Ruby的標記過程。
Python中的GC閾值
Python什么時候會進行這個標記過程?隨着你的程序運行,Python解釋器保持對新創建的對象,以及因為引用計數為零而被釋放掉的對象的追蹤。從理論上說,這兩個值應該保持一致,因為程序新建的每個對象都應該最終被釋放掉。
當然,事實並非如此。因為循環引用的原因,並且因為你的程序使用了一些比其他對象存在時間更長的對象,從而被分配對象的計數值與被釋放對象的計數值之間的差異在逐漸增長。一旦這個差異累計超過某個閾值,則Python的收集機制就啟動了,並且觸發上邊所說到的零代算法,釋放“浮動的垃圾”,並且將剩下的對象移動到一代列表。
隨着時間的推移,程序所使用的對象逐漸從零代列表移動到一代列表。而Python對於一代列表中對象的處理遵循同樣的方法,一旦被分配計數值與被釋放計數值累計到達一定閾值,Python會將剩下的活躍對象移動到二代列表。
通過這種方法,你的代碼所長期使用的對象,那些你的代碼持續訪問的活躍對象,會從零代鏈表轉移到一代再轉移到二代。通過不同的閾值設置,Python可以在不同的時間間隔處理這些對象。Python處理零代最為頻繁,其次是一代然后才是二代。
檢測循環引用源碼分析:(以list為例)
垃圾標記時(也就是檢測循環引用時),先將集合中對象的引用計數復制一份副本(以免在操作過程中破壞真實的引用計數值)
創建container的過程: container對象 = pyGC_Head | PyObject_HEAD | Container Object
// gcmodule.c
static void
update_refs(PyGC_Head *containers)
{
PyGC_Head *gc = containers->gc.gc_next; //實現gc的頭指針的復制,賦值給PyGC_Head 指針 gc
for (; gc != containers; gc = gc->gc.gc_next) { // gc是List的頭部,List的具體數值在gc的后面,
//所以for循環結束的條件就是 gc != containers(這里的containers就是list)
assert(gc->gc.gc_refs == GC_REACHABLE);
gc->gc.gc_refs = FROM_GC(gc)->ob_refcnt;
assert(gc->gc.gc_refs != 0);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
這個traverse是對象類型定義的函數,用來遍歷對象,通過傳入的回調函數visit_decref來操作引用計數副本。
例如dict就要在key和value上都用visit_decref操作一遍:
// dictobject.c
static int
dict_traverse(PyObject *op, visitproc visit, void *arg)
{
Py_ssize_t i = 0;
PyObject *pk;
PyObject *pv;
while (PyDict_Next(op, &i, &pk, &pv)) {
visit(pk);
visit(pv);
}
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
然后根據引用計數副本值是否為0將集合內的對象分成兩類,reachable和unreachable,其中unreachable是可以被回收的對象:
// gcmodule.c
static void
move_unreachable(PyGC_Head *young, PyGC_Head *unreachable)
{
PyGC_Head *gc = young->gc.gc_next;
while (gc != young) {
PyGC_Head *next;
if (gc->gc.gc_refs) {
PyObject *op = FROM_GC(gc);
traverseproc traverse = op->ob_type->tp_traverse;
assert(gc->gc.gc_refs > 0);
gc->gc.gc_refs = GC_REACHABLE;
(void) traverse(op,
(visitproc)visit_reachable,
(void *)young);
next = gc->gc.gc_next;
}
else {
next = gc->gc.gc_next;
gc_list_move(gc, unreachable);
gc->gc.gc_refs = GC_TENTATIVELY_UNREACHABLE;
}
gc = next;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
在處理了weak reference和finalizer等瑣碎細節后(本文不展開講述,有興趣的童鞋請參考python源碼),就可以回收unreachable中的對象了。
弱代假說
來看看代垃圾回收算法的核心行為:垃圾回收器會更頻繁的處理新對象。一個新的對象即是你的程序剛剛創建的,而一個來的對象則是經過了幾個時間周期之后仍然存在的對象。Python會在當一個對象從零代移動到一代,或是從一代移動到二代的過程中提升(promote)這個對象。
為什么要這么做?這種算法的根源來自於弱代假說(weak generational hypothesis)。這個假說由兩個觀點構成:首先是年親的對象通常死得也快,而老對象則很有可能存活更長的時間。
假定現在我用Python或是Ruby創建一個新對象 n1=”ABC”:
根據假說,我的代碼很可能僅僅會使用ABC很短的時間。這個對象也許僅僅只是一個方法中的中間結果,並且隨着方法的返回這個對象就將變成垃圾了。大部分的新對象都是如此般地很快變成垃圾。然而,偶爾程序會創建一些很重要的,存活時間比較長的對象-例如web應用中的session變量或是配置項。
通過頻繁的處理零代鏈表中的新對象,Python的垃圾收集器將把時間花在更有意義的地方:它處理那些很快就可能變成垃圾的新對象。同時只在很少的時候,當滿足閾值的條件,收集器才回去處理那些老變量。
三、分代回收
先給出gc的邏輯:(重點)
分配內存
-> 發現超過閾值了
-> 觸發垃圾回收
-> 將所有可收集對象鏈表放到一起
-> 遍歷, 計算有效引用計數
-> 分成 有效引用計數=0 和 有效引用計數 > 0 兩個集合
-> 大於0的, 放入到更老一代
-> =0的, 執行回收
-> 回收遍歷容器內的各個元素, 減掉對應元素引用計數(破掉循環引用)
-> 執行-1的邏輯, 若發現對象引用計數=0, 觸發內存回收
-> python底層內存管理機制回收內存
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
Python中, 引入了分代收集, 總共三個”代”. Python 中, 一個代就是一個鏈表, 所有屬於同一”代”的內存塊都鏈接在同一個鏈表中
用來表示“代”的結構體是gc_generation, 包括了當前代鏈表表頭、對象數量上限、當前對象數量:
// gcmodule.c
struct gc_generation {
PyGC_Head head;
int threshold; /* collection threshold */
int count; /* count of allocations or collections of younger generations */
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
Python默認定義了三代對象集合,索引數越大,對象存活時間越長
#define NUM_GENERATIONS 3 #define GEN_HEAD(n) (&generations[n].head) /* linked lists of container objects */ static struct gc_generation generations[NUM_GENERATIONS] = { /* PyGC_Head, threshold, count */ {{{GEN_HEAD(0), GEN_HEAD(0), 0}}, 700, 0}, {{{GEN_HEAD(1), GEN_HEAD(1), 0}}, 10, 0}, {{{GEN_HEAD(2), GEN_HEAD(2), 0}}, 10, 0}, };
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
新生成的對象會被加入第0代,前面_PyObject_GC_Malloc中省略的部分就是Python GC觸發的時機。每新生成一個對象都會檢查第0代有沒有滿,如果滿了就開始着手進行垃圾回收.
g->gc.gc_refs = GC_UNTRACKED;
generations[0].count++; /* number of allocated GC objects */
if (generations[0].count > generations[0].threshold &&
enabled &&
generations[0].threshold &&
!collecting &&
!PyErr_Occurred()) {
collecting = 1;
collect_generations();
collecting = 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
分代回收總結:
分代回收是一種以空間換時間的操作方式,Python將內存根據對象的存活時間划分為不同的集合,每個集合稱為一個代,Python將內存分為了3“代”,分別為年輕代(第0代)、中年代(第1代)、老年代(第2代),他們對應的是3個鏈表,它們的垃圾收集頻率與對象的存活時間的增大而減小。新創建的對象都會分配在年輕代,年輕代鏈表的總數達到上限時,Python垃圾收集機制就會被觸發,把那些可以被回收的對象回收掉,而那些不會回收的對象就會被移到中年代去,依此類推,老年代中的對象是存活時間最久的對象,甚至是存活於整個系統的生命周期內。同時,分代回收是建立在標記清除技術基礎之上。分代回收同樣作為Python的輔助垃圾收集技術處理那些容器對象.
美好的一天,從閱讀源碼開始!
<li class="tool-item tool-active is-like "><a href="javascript:;"><svg class="icon" aria-hidden="true">
<use xlink:href="#csdnc-thumbsup"></use>
</svg><span class="name">點贊</span>
<span class="count">16</span>
</a></li>
<li class="tool-item tool-active is-collection "><a href="javascript:;" data-report-click="{"mod":"popu_824"}"><svg class="icon" aria-hidden="true">
<use xlink:href="#icon-csdnc-Collection-G"></use>
</svg><span class="name">收藏</span></a></li>
<li class="tool-item tool-active is-share"><a href="javascript:;" data-report-click="{"mod":"1582594662_002"}"><svg class="icon" aria-hidden="true">
<use xlink:href="#icon-csdnc-fenxiang"></use>
</svg>分享</a></li>
<!--打賞開始-->
<!--打賞結束-->
<li class="tool-item tool-more">
<a>
<svg t="1575545411852" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5717" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M179.176 499.222m-113.245 0a113.245 113.245 0 1 0 226.49 0 113.245 113.245 0 1 0-226.49 0Z" p-id="5718"></path><path d="M509.684 499.222m-113.245 0a113.245 113.245 0 1 0 226.49 0 113.245 113.245 0 1 0-226.49 0Z" p-id="5719"></path><path d="M846.175 499.222m-113.245 0a113.245 113.245 0 1 0 226.49 0 113.245 113.245 0 1 0-226.49 0Z" p-id="5720"></path></svg>
</a>
<ul class="more-box">
<li class="item"><a class="article-report">文章舉報</a></li>
</ul>
</li>
</ul>
</div>
</div>
<div class="person-messagebox">
<div class="left-message"><a href="https://blog.csdn.net/xiongchengluo1129">
<img src="https://profile.csdnimg.cn/D/2/8/3_xiongchengluo1129" class="avatar_pic" username="xiongchengluo1129">
<img src="https://g.csdnimg.cn/static/user-reg-year/2x/4.png" class="user-years">
</a></div>
<div class="middle-message">
<div class="title"><span class="tit"><a href="https://blog.csdn.net/xiongchengluo1129" data-report-click="{"mod":"popu_379"}" target="_blank">cool whidpers</a></span>
</div>
<div class="text"><span>發布了72 篇原創文章</span> · <span>獲贊 158</span> · <span>訪問量 41萬+</span></div>
</div>
<div class="right-message">
<a href="https://im.csdn.net/im/main.html?userName=xiongchengluo1129" target="_blank" class="btn btn-sm btn-red-hollow bt-button personal-letter">私信
</a>
<a class="btn btn-sm bt-button personal-watch" data-report-click="{"mod":"popu_379"}">關注</a>
</div>
</div>
</div>
</article>