python垃圾回收

python作為一門解釋型語言,以代碼簡潔易懂著稱。我們可以直接對名稱賦值,而不必聲明類型。名稱類型的確定、內存空間的分配與釋放都是由python解釋器在運行時進行的。python這一自動管理內存功能極大的減小了程序員負擔,這也是成就python自身的重要原因之一。所以,這一篇文章我們就聊一聊python的內存管理。

引用計數

Python中,主要通過引用計數(Reference Counting)進行垃圾回收。

typedef struct_object {
 int ob_refcnt;
 struct_typeobject *ob_type;
} PyObject;

在Python中每一個對象的核心就是一個結構體PyObject,它的內部有一個引用計數器(ob_refcnt)。程序在運行的過程中會實時的更新ob_refcnt的值,來反映引用當前對象的名稱數量。當某對象的引用計數值為0,那么它的內存就會被立即釋放掉。

以下情況是導致引用計數加一的情況:

  • 對象被創建,例如a=2
  • 對象被引用,b=a
  • 對象被作為參數,傳入到一個函數中
  • 對象作為一個元素,存儲在容器中

下面的情況則會導致引用計數減一:

  • 對象別名被顯示銷毀 del
  • 對象別名被賦予新的對象
  • 一個對象離開他的作用域
  • 對象所在的容器被銷毀或者是從容器中刪除對象

我們還可以通過sys包中的getrefcount()來獲取一個名稱所引用的對象當前的引用計數(注意,這里getrefcount()本身會使得引用計數加一)

sys.getrefcount(a)

引用計數法有其明顯的優點,如高效、實現邏輯簡單、具備實時性,一旦一個對象的引用計數歸零,內存就直接釋放了。不用像其他機制等到特定時機。將垃圾回收隨機分配到運行的階段,處理回收內存的時間分攤到了平時,正常程序的運行比較平穩。但是,引用計數也存在著一些缺點,通常的缺點有:

  • 邏輯簡單,但實現有些麻煩。每個對象需要分配單獨的空間來統計引用計數,這無形中加大的空間的負擔,并且需要對引用計數進行維護,在維護的時候很容易會出錯。
  • 在一些場景下,可能會比較慢。正常來說垃圾回收會比較平穩運行,但是當需要釋放一個大的對象時,比如字典,需要對引用的所有對象循環嵌套調用,從而可能會花費比較長的時間。
  • 循環引用。這將是引用計數的致命傷,引用計數對此是無解的,因此必須要使用其它的垃圾回收算法對其進行補充。

也就是說,Python 的垃圾回收機制,很大一部分是為了處理可能產生的循環引用,是對引用計數的補充。

標記清除解決循環引用

Python采用了“標記-清除”(Mark and Sweep)算法,解決容器對象可能產生的循環引用問題。(注意,只有容器對象才會產生循環引用的情況,比如列表、字典、用戶自定義類的對象、元組等。而像數字,字符串這類簡單類型不會出現循環引用。作為一種優化策略,對于只包含簡單類型的元組也不在標記清除算法的考慮之列)

跟其名稱一樣,該算法在進行垃圾回收時分成了兩步,分別是:

  • A)標記階段,遍歷所有的對象,如果是可達的(reachable),也就是還有對象引用它,那么就標記該對象為可達;
  • B)清除階段,再次遍歷對象,如果發現某個對象沒有標記為可達,則就將其回收。

如下圖所示,在標記清除算法中,為了追蹤容器對象,需要每個容器對象維護兩個額外的指針,用來將容器對象組成一個雙端鏈表,指針分別指向前后兩個容器對象,方便插入和刪除操作。python解釋器(Cpython)維護了兩個這樣的雙端鏈表,一個鏈表存放著需要被掃描的容器對象,另一個鏈表存放著臨時不可達對象。在圖中,這兩個鏈表分別被命名為”Object to Scan”和”Unreachable”。圖中例子是這么一個情況:link1,link2,link3組成了一個引用環,同時link1還被一個變量A(其實這里稱為名稱A更好)引用。link4自引用,也構成了一個引用環。從圖中我們還可以看到,每一個節點除了有一個記錄當前引用計數的變量ref_count還有一個gc_ref變量,這個gc_ref是ref_count的一個副本,所以初始值為ref_count的大小。

image

gc啟動的時候,會逐個遍歷”Object to Scan”鏈表中的容器對象,并且將當前對象所引用的所有對象的gc_ref減一。(掃描到link1的時候,由于link1引用了link2,所以會將link2的gc_ref減一,接著掃描link2,由于link2引用了link3,所以會將link3的gc_ref減一…..)像這樣將”Objects to Scan”鏈表中的所有對象考察一遍之后,兩個鏈表中的對象的ref_count和gc_ref的情況如下圖所示。這一步操作就相當于解除了循環引用對引用計數的影響。

image

接著,gc會再次掃描所有的容器對象,如果對象的gc_ref值為0,那么這個對象就被標記為GC_TENTATIVELY_UNREACHABLE,并且被移至”Unreachable”鏈表中。下圖中的link3和link4就是這樣一種情況。

image

如果對象的gc_ref不為0,那么這個對象就會被標記為GC_REACHABLE。同時當gc發現有一個節點是可達的,那么他會遞歸式的將從該節點出發可以到達的所有節點標記為GC_REACHABLE,這就是下圖中link2和link3所碰到的情形。

image

除了將所有可達節點標記為GC_REACHABLE之外,如果該節點當前在”Unreachable”鏈表中的話,還需要將其移回到”Object to Scan”鏈表中,下圖就是link3移回之后的情形。

image

第二次遍歷的所有對象都遍歷完成之后,存在于”Unreachable”鏈表中的對象就是真正需要被釋放的對象。如上圖所示,此時link4存在于Unreachable鏈表中,gc隨即釋放之。

上面描述的垃圾回收的階段,會暫停整個應用程序,等待標記清除結束后才會恢復應用程序的運行。

分代回收

在循環引用對象的回收中,整個應用程序會被暫停,為了減少應用程序暫停的時間,Python 通過“分代回收”(Generational Collection)以空間換時間的方法提高垃圾回收效率。

分代回收是基于這樣的一個統計事實,對于程序,存在一定比例的內存塊的生存周期比較短;而剩下的內存塊,生存周期會比較長,甚至會從程序開始一直持續到程序結束。生存期較短對象的比例通常在 80%~90% 之間,這種思想簡單點說就是:對象存在時間越長,越可能不是垃圾,應該越少去收集。這樣在執行標記-清除算法時可以有效減小遍歷的對象數,從而提高垃圾回收的速度。

python gc給對象定義了三種世代(0,1,2),每一個新生對象在generation zero中,如果它在一輪gc掃描中活了下來,那么它將被移至generation one,在那里他將較少的被掃描,如果它又活過了一輪gc,它又將被移至generation two,在那里它被掃描的次數將會更少。

gc的掃描在什么時候會被觸發呢?答案是當某一世代中被分配的對象與被釋放的對象之差達到某一閾值的時候,就會觸發gc對某一世代的掃描。值得注意的是當某一世代的掃描被觸發的時候,比該世代年輕的世代也會被掃描。也就是說如果世代2的gc掃描被觸發了,那么世代0,世代1也將被掃描,如果世代1的gc掃描被觸發,世代0也會被掃描。

該閾值可以通過下面兩個函數查看和調整:

gc.get_threshold() # (threshold0, threshold1, threshold2).
gc.set_threshold(threshold0[, threshold1[, threshold2]])

下面對set_threshold()中的三個參數threshold0, threshold1, threshold2進行介紹。gc會記錄自從上次收集以來新分配的對象數量與釋放的對象數量,當兩者之差超過threshold0的值時,gc的掃描就會啟動,初始的時候只有世代0被檢查。如果自從世代1最近一次被檢查以來,世代0被檢查超過threshold1次,那么對世代1的檢查將被觸發。相同的,如果自從世代2最近一次被檢查以來,世代1被檢查超過threshold2次,那么對世代2的檢查將被觸發。get_threshold()是獲取三者的值,默認值為(700,10,10).

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 前言 一般面試python的時候談到垃圾回收機制,我們的回答可能就是簡單的:引用計數、標記清除和分代回收。本文就圍...
    冰闊落jack閱讀 292評論 0 0
  • python作為一門解釋型語言,以代碼簡潔易懂著稱。我們可以直接對名稱賦值,而不必聲明類型。名稱類型的確定、內存空...
    宇哥聊AI閱讀 4,215評論 2 7
  • 垃圾回收機制一般有兩個階段:垃圾檢測和垃圾回收。Python GC 主要使用引用計數來跟蹤和垃圾回收。在引用計數的...
    vckah閱讀 263評論 0 0
  • Python的GC模塊主要運用了“引用計數”(reference counting)來跟蹤和回收垃圾。在引用計數的...
    dpengwang閱讀 288評論 1 0
  • 16宿命:用概率思維提高你的勝算 以前的我是風險厭惡者,不喜歡去冒險,但是人生放棄了冒險,也就放棄了無數的可能。 ...
    yichen大刀閱讀 6,099評論 0 4