python垃圾回收

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

引用計(jì)數(shù)

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

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

在Python中每一個(gè)對(duì)象的核心就是一個(gè)結(jié)構(gòu)體PyObject,它的內(nèi)部有一個(gè)引用計(jì)數(shù)器(ob_refcnt)。程序在運(yùn)行的過程中會(huì)實(shí)時(shí)的更新ob_refcnt的值,來反映引用當(dāng)前對(duì)象的名稱數(shù)量。當(dāng)某對(duì)象的引用計(jì)數(shù)值為0,那么它的內(nèi)存就會(huì)被立即釋放掉。

以下情況是導(dǎo)致引用計(jì)數(shù)加一的情況:

  • 對(duì)象被創(chuàng)建,例如a=2
  • 對(duì)象被引用,b=a
  • 對(duì)象被作為參數(shù),傳入到一個(gè)函數(shù)中
  • 對(duì)象作為一個(gè)元素,存儲(chǔ)在容器中

下面的情況則會(huì)導(dǎo)致引用計(jì)數(shù)減一:

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

我們還可以通過sys包中的getrefcount()來獲取一個(gè)名稱所引用的對(duì)象當(dāng)前的引用計(jì)數(shù)(注意,這里getrefcount()本身會(huì)使得引用計(jì)數(shù)加一)

sys.getrefcount(a)

引用計(jì)數(shù)法有其明顯的優(yōu)點(diǎn),如高效、實(shí)現(xiàn)邏輯簡(jiǎn)單、具備實(shí)時(shí)性,一旦一個(gè)對(duì)象的引用計(jì)數(shù)歸零,內(nèi)存就直接釋放了。不用像其他機(jī)制等到特定時(shí)機(jī)。將垃圾回收隨機(jī)分配到運(yùn)行的階段,處理回收內(nèi)存的時(shí)間分?jǐn)偟搅似綍r(shí),正常程序的運(yùn)行比較平穩(wěn)。但是,引用計(jì)數(shù)也存在著一些缺點(diǎn),通常的缺點(diǎn)有:

  • 邏輯簡(jiǎn)單,但實(shí)現(xiàn)有些麻煩。每個(gè)對(duì)象需要分配單獨(dú)的空間來統(tǒng)計(jì)引用計(jì)數(shù),這無形中加大的空間的負(fù)擔(dān),并且需要對(duì)引用計(jì)數(shù)進(jìn)行維護(hù),在維護(hù)的時(shí)候很容易會(huì)出錯(cuò)。
  • 在一些場(chǎng)景下,可能會(huì)比較慢。正常來說垃圾回收會(huì)比較平穩(wěn)運(yùn)行,但是當(dāng)需要釋放一個(gè)大的對(duì)象時(shí),比如字典,需要對(duì)引用的所有對(duì)象循環(huán)嵌套調(diào)用,從而可能會(huì)花費(fèi)比較長(zhǎng)的時(shí)間。
  • 循環(huán)引用。這將是引用計(jì)數(shù)的致命傷,引用計(jì)數(shù)對(duì)此是無解的,因此必須要使用其它的垃圾回收算法對(duì)其進(jìn)行補(bǔ)充。

也就是說,Python 的垃圾回收機(jī)制,很大一部分是為了處理可能產(chǎn)生的循環(huán)引用,是對(duì)引用計(jì)數(shù)的補(bǔ)充。

標(biāo)記清除解決循環(huán)引用

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

跟其名稱一樣,該算法在進(jìn)行垃圾回收時(shí)分成了兩步,分別是:

  • A)標(biāo)記階段,遍歷所有的對(duì)象,如果是可達(dá)的(reachable),也就是還有對(duì)象引用它,那么就標(biāo)記該對(duì)象為可達(dá);
  • B)清除階段,再次遍歷對(duì)象,如果發(fā)現(xiàn)某個(gè)對(duì)象沒有標(biāo)記為可達(dá),則就將其回收。

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

image

gc啟動(dòng)的時(shí)候,會(huì)逐個(gè)遍歷”O(jiān)bject to Scan”鏈表中的容器對(duì)象,并且將當(dāng)前對(duì)象所引用的所有對(duì)象的gc_ref減一。(掃描到link1的時(shí)候,由于link1引用了link2,所以會(huì)將link2的gc_ref減一,接著掃描link2,由于link2引用了link3,所以會(huì)將link3的gc_ref減一…..)像這樣將”O(jiān)bjects to Scan”鏈表中的所有對(duì)象考察一遍之后,兩個(gè)鏈表中的對(duì)象的ref_count和gc_ref的情況如下圖所示。這一步操作就相當(dāng)于解除了循環(huán)引用對(duì)引用計(jì)數(shù)的影響。

image

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

image

如果對(duì)象的gc_ref不為0,那么這個(gè)對(duì)象就會(huì)被標(biāo)記為GC_REACHABLE。同時(shí)當(dāng)gc發(fā)現(xiàn)有一個(gè)節(jié)點(diǎn)是可達(dá)的,那么他會(huì)遞歸式的將從該節(jié)點(diǎn)出發(fā)可以到達(dá)的所有節(jié)點(diǎn)標(biāo)記為GC_REACHABLE,這就是下圖中l(wèi)ink2和link3所碰到的情形。

image

除了將所有可達(dá)節(jié)點(diǎn)標(biāo)記為GC_REACHABLE之外,如果該節(jié)點(diǎn)當(dāng)前在”Unreachable”鏈表中的話,還需要將其移回到”O(jiān)bject to Scan”鏈表中,下圖就是link3移回之后的情形。

image

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

上面描述的垃圾回收的階段,會(huì)暫停整個(gè)應(yīng)用程序,等待標(biāo)記清除結(jié)束后才會(huì)恢復(fù)應(yīng)用程序的運(yùn)行。

分代回收

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

分代回收是基于這樣的一個(gè)統(tǒng)計(jì)事實(shí),對(duì)于程序,存在一定比例的內(nèi)存塊的生存周期比較短;而剩下的內(nèi)存塊,生存周期會(huì)比較長(zhǎng),甚至?xí)某绦蜷_始一直持續(xù)到程序結(jié)束。生存期較短對(duì)象的比例通常在 80%~90% 之間,這種思想簡(jiǎn)單點(diǎn)說就是:對(duì)象存在時(shí)間越長(zhǎng),越可能不是垃圾,應(yīng)該越少去收集。這樣在執(zhí)行標(biāo)記-清除算法時(shí)可以有效減小遍歷的對(duì)象數(shù),從而提高垃圾回收的速度。

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

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

該閾值可以通過下面兩個(gè)函數(shù)查看和調(diào)整:

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

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

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

友情鏈接更多精彩內(nèi)容