1. 程式人生 > >Python垃圾回收機制

Python垃圾回收機制

num 行為 解決 處理 滿足 鏈表合並 內存管理機制 不能 struct

對於Python垃圾回收機制主要有三個,首先是使用引用計數來跟蹤和回收垃圾,為了解決循環
引用問題,就采用標記-清除的方法,標記-清除的方法所帶來的額外操作實際上與系統中總的內存
塊的總數是相關的,當需要回收的內存塊越多,垃圾檢查帶來的額外操作就越多,為了提高垃圾收集
的效率,采用“空間換時間的策略”,即使用分代機制,對於長時間沒有被回收的內存就減少對它的
垃圾回收效率。

首先看一下Python的內存管理架構:

layer 3: Object-specific memory(int/dict/list/string....)
Python 實現並維護
更高抽象層次的內存管理策略, 主要是各類特定對象的緩沖池機制

layer 
2: Pythons object allocator Python 實現並維護 實現了創建/銷毀Python對象的接口(PyObject_New/Del), 涉及對象參數/引用計數等 layer 1: Pythons raw memory allocator (PyMem_ API) Python 實現並維護, 包裝了第0層的內存管理接口, 提供統一的raw memory管理接口 封裝的原因: 不同操作系統 C 行為不一定一致, 保證可移植性, 相同語義相同行為 layer 0: Underlying general-purpose allocator (ex: C library malloc) 操作系統提供的內存管理接口, 由操作系統實現並管理, Python不能幹涉這一層的行為

引用計數機制

引用計數是一種垃圾收集機制,而且也是一種最直觀,最簡單的垃圾回收技術 當一個對象的引用被創建或者復制時,對象的引用計數加1;當一個對象的引用被銷毀 對象的引用計數減1。如果對象的引用計數減少為0,那麽就意味著對象已經不會被任何人使用,可以將其
所占有的內存釋放。
引用計數機制的優點:實時性,對於任何內存一旦沒有指向它的引用,就會立即被回收(這裏需要滿足閾值才可以)
引用計數機制的缺點:引用計數機制所帶來的維護引用計數的額外操作與Python運行中所運行的內存分配和釋放,引用賦值的
次數是成正比的,為了與引用計數機制搭配,在內存的分配和釋放上獲得最高的效率,Python設計了大量的
內存池機制,減少運行期間malloc和free的操作。

>>> from sys import getrefcount
>>> a = [1,2,3]
>>> getrefcount(a)
2
>>> b =a
>>> getrefcount(a)
3
>>>

標記-清除機制

引用計數機制有個致命的弱點,就是可能存在循環引用的問題:
一組對象的引用計數都不為0,然而這些對象實際上並沒有被任何外部變量引用,它們之間只是相互引用,這意味這個不會
有人使用這組對象,應該回收這些對象所占的內存,然後由於互相引用的存在, 每個對象的引用計數都不為0,因此這些對象
所占用的內存永遠不會被回收。
標記-清除機制就是為了解決循環引用的問題。首先只有container對象之間才會產生循環引用,所謂container對象即是內部
可持有對其他對象的引用的對象,比如list、dict、class等,而像PyIntObject、PyStringObject這些是絕不可能產生循環引用的
所以Python的垃圾回收機制運行時,只需要檢查這些container對象,為了跟蹤每個container,需要將這些對象組織到一個集合中。
Python采用了一個雙向鏈表,所以的container對象在創建之後,就會被插入到這個鏈表中。這個鏈表也叫作可收集對象鏈表。

為了解決循環引用的問題,提出了有效引用計數的概念,即循環引用的兩個對象引用計數不為0,實際上有效的引用計數為0
假設兩個對象為A、B,我們從A出發,因為它有一個對B的引用,則將B的引用計數減1;然後順著引用達到B,因為B有一個對A的引用,
同樣將A的引用減1,這樣,就完成了循環引用對象間環摘除。但是這樣直接修改真實的引用計數,可能存在懸空引用的問題。
所以采用修改計數計數副本的方法。
這個計數副本的唯一作用是尋找root object集合(該集合中的對象是不能被回收的)。當成功尋找到root object集合之後,
我們就可以從root object出發,沿著引用鏈,一個接一個的標記不能回收的內存。首先將現在的內存鏈表一分為二,
一條鏈表中維護root object集合,成為root鏈表,而另外一條鏈表中維護剩下的對象,成為unreachable鏈表。之所以要剖成兩個鏈表,
是基於這樣的一種考慮:現在的unreachable可能存在被root鏈表中的對象,直接或間接引用的對象,這些對象是不能被回收的,
一旦在標記的過程中,發現這樣的對象,就將其從unreachable鏈表中移到root鏈表中;當完成標記後,unreachable鏈表中剩下
的所有對象就是名副其實的垃圾對象了,接下來的垃圾回收只需限制在unreachable鏈表中即可。

分代回收

分代回收的思想:將系統中的所有內存塊根據其存活時間劃分為不同的集合,每一個集合就稱為一個“代”
垃圾收集的頻率隨著“代”的存活時間的增大而減小,也就是說,活的越長的對象,就越可能不是垃圾,就應該
越少去收集。當某一代對象經歷過垃圾回收,依然存活,那麽它就被歸入下一代中。
在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}, //700個container, 超過立即觸發垃圾回收機制
{{{GEN_HEAD(1), GEN_HEAD(1), 0}}, 10, 0}, // 10個
{{{GEN_HEAD(2), GEN_HEAD(2), 0}}, 10, 0}, // 10個
};

PyGC_Head *_PyGC_generation0 = GEN_HEAD(0);

其中存在三個閾值,分別是700,10,10
可以通過get_threshold()方法獲得閾值:

import gc
print(gc.get_threshold())
(700, 10, 10) 

其中第一個閾值表示第0代鏈表最多可以容納700個container對象,超過了這個極限值,就會立即出發垃圾回收機制。

後面兩個閾值10是分代有關系,就是每10次0代垃圾回收,會配合1次1代的垃圾回收;而每10次1代的垃圾回收,
才會有1次的2代垃圾回收。也就是空間換時間的體現。


垃圾回收的流程:
--> 分配內存的時候發現超過閾值(第0代的container個數),觸發垃圾回收
--> 將所有可收集對象鏈表放在一起(將比當前處理的“代”更年輕的"代"的鏈表合並到當前”代“中)
--> 計算有效引用計數
--> 根據有效引用計數分為計數等於0和大於0兩個集合
--> 引用計數大於0的對象,放入下一代
--> 引用計數等於0的對象,執行回收
--> 回收遍歷容器內的各個元素, 減掉對應元素引用計數(破掉循環引用)
--> python底層內存管理機制回收內存

參考文檔:
http://www.cnblogs.com/vamei/p/3232088.html
http://python.jobbole.com/83548/
http://python.jobbole.com/82061/
python源碼剖析

Python垃圾回收機制