1. 程式人生 > >Python -- 其他知識點(垃圾回收機制)

Python -- 其他知識點(垃圾回收機制)

垃圾回收機制(Garbage collection)

一、小整數物件池

整數在程式中的使用非常廣泛,Python為了優化速度,使用了小整數物件池, 避免為整數頻繁申請和銷燬記憶體空間。

Python 對小整數的定義是 [-5, 257) 這些整數物件是提前建立好的,不會被垃圾回收。在一個 Python 的程式中,所有位於這個範圍內的整數使用的都是同一個物件。.

同理,單個字母也是這樣的。但是當定義2個相同的字串時,引用計數為0,將會觸發垃圾回收機制。

二、大整數物件池

每一個大整數,均建立一個新的物件。

三、intern機制

a1 = "HelloWorld"
a2 = "HelloWorld"
a3 = "HelloWorld"
a4 = "HelloWorld"
a5 = "HelloWorld"
a6 = "HelloWorld"
a7 = "HelloWorld"
a8 = "HelloWorld"
a9 = "HelloWorld"

python會不會建立9個物件呢?在記憶體中會不會開闢9個”HelloWorld”的記憶體空間呢? 想象下,如果是這樣的話,我們寫10000個物件,比如a1=”HelloWorld”…..a1000=”HelloWorld”, 那他豈不是開闢了1000個”HelloWorld”所佔的記憶體空間了呢?如果真這樣,記憶體不就爆了嗎?所以python中有這樣一個機制—— intern機制 ,讓他只佔用一個”HelloWorld”所佔的記憶體空間。靠引用計數去維護何時釋放。

小結:

     1. 小整數[-5,257)共用物件,常駐記憶體
     2. 單個字元共用物件,常駐記憶體
     3. 單個單詞,不可修改,預設開啟intern機制,共用物件,如果引用計數為0,則銷燬。

     注:字串(含有空格),不可修改,沒有開啟intern機制,不共享物件,如果引用計數為0,則銷燬該物件。

四、垃圾回收機制原理

Python裡也同java一樣採用了垃圾收集機制,不過不一樣的是: Python採用的是引用計數機制為主,分代收集兩種機制為輔的策略。

4.1、引用計數機制:

Python裡每一個東西都是物件,它們的核心就是一個結構體: PyObject

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

PyObject是每個物件必有的內容,其中ob_refcnt

就是做為引用計數。當一個物件有新的引用時,它的ob_refcnt就會增加,當引用它的物件被刪除,它的ob_refcnt就會減少。

#define Py_INCREF(op) ((op)->ob_refcnt++) //增加計數
#define Py_DECREF(op) \ //減少計數
if (--(op)->ob_refcnt != 0) \
; \
else \
__Py_Dealloc((PyObject *)(op))

當引用計數為0時,該物件生命就結束了。

4.2、引用計數機制的優點:

1、簡單

2、實時性:一旦沒有引用,記憶體就直接釋放了。不用像其他機制等到特定時機。實時性還帶來一個好處:處理回收記憶體的時間分攤到了平時。

4.3、引用計數機制的缺點:

1、維護引用計數消耗資源

2、迴圈引用

五、在Ruby和Python中視覺化垃圾收集

應用程式的跳動之心

GC系統不僅僅是“收集垃圾”。事實上,它們執行三項重要任務。他們

  • 為新物件分配記憶體,
  • 識別垃圾物件,和
  • 從垃圾物件中回收記憶體。

想象一下,如果您的應用程式是一個人體:您編寫的所有優雅程式碼,您的業務邏輯,您的演算法,將是應用程式內部的大腦或智慧。按照這個比喻,你認為垃圾收集器的身體部位是什麼?[我從RuPy觀眾那裡得到了很多有趣的答案:腎臟,白細胞:)]

我認為垃圾收集器是您應用程式的跳動核心。正如您的心臟為身體的其他部分提供血液和營養一樣,垃圾收集器為您的應用程式提供記憶體和物件供您使用。如果你的心臟停止跳動,你會在幾秒鐘內死亡。如果垃圾收集器停止或執行緩慢 - 如果它堵塞了動脈 - 你的應用程式將減速並最終死亡!

一個簡單的例子

使用示例來理解理論總是有幫助的。這是一個用Python和Ruby編寫的簡單類,我們今天可以用它作為例子:

順便說一句,令我驚訝的是這個程式碼在兩種語言中的相似之處:Ruby和Python實際上只是略有不同的說法。但語言也實現在國內類似的方式?

免費清單

當我們呼叫上面的Node.new(1)時,Ruby究竟做了什麼?Ruby如何為我們建立一個新物件?

令人驚訝的是,它確實很少!事實上,在您的程式碼開始執行之前很久,Ruby就會提前建立數千個物件並將它們放在一個名為空閒列表的連結串列上。從概念上講,這是免費列表的樣子:

想象一下,上面的每個白色方塊都是一個未使用的,預先建立的Ruby物件。當我們呼叫Node.new時,Ruby只需要將其中一個物件交給我們:

在上圖中,左側的灰色方塊表示我們在程式碼中使用的活動Ruby物件,而剩餘的白色方塊是未使用的物件。[注意:當然,我的圖表是現實的簡化版本。事實上,Ruby會使用另一個物件來儲存字串“ABC”,第三個物件用於儲存Node的類定義,還有其他物件用於儲存我的程式碼的解析後的抽象語法樹(AST)表示等。

如果我們再次呼叫Node.new,Ruby只會給我們另一個物件:


John McCarthy 1960年實施的Lisp包含
了第一個垃圾收集器。
 (麻省理工學院博物館提供)

50年前,一位名叫John McCarthy的傳奇電腦科學家發明了這種使用預先建立物件連結串列的簡單演算法,同時他正在研究Lisp的原始實現。Lisp不僅是最早的函數語言程式設計語言之一,還包含了電腦科學領域的其他一些突破性進展。其中之一是使用垃圾收集自動管理應用程式記憶體的概念。

Ruby的標準版本,也稱為“Matz的Ruby直譯器”(MRI),使用類似於McCarthy在1960年實現Lisp時使用的GC演算法。無論好壞,Ruby使用53年的垃圾收集演算法。正如Lisp所做的那樣,Ruby會提前建立物件,並在分配新物件或值時將它們交給您的程式碼。

在Python中分配物件

我們已經看到Ruby提前建立物件並將它們儲存在空閒列表中。那Python怎麼樣?

雖然Python在內部也出於各種原因使用空閒列表(它會回收某些物件,例如列表),但它通常會為新物件和值分配記憶體,而不像Ruby那樣。

假設我們使用Python建立一個Node物件:

與Ruby不同,Python會在您建立物件時立即向作業系統詢問記憶體。(Python實際上實現了自己的記憶體分配系統,它在作業系統堆之上提供了額外的抽象層。但是我今天沒有時間進入這些細節。)

當我們建立第二個物件時,Python將再次向作業系統詢問更多記憶體:


Ruby會在
記憶體中留下未使用的物件,直到下一個GC程序執行。

看似簡單; 在我們建立物件的那一刻,Python花時間為我們查詢和分配記憶體。

Ruby開發人員住在凌亂的房子裡

回到Ruby。隨著我們分配越來越多的物件,Ruby將繼續從自由列表中提取預先建立的物件。在這樣做時,免費列表將變短:

......而且更短:

請注意,當我繼續為n1分配新值時,Ruby會留下舊值。ABC,JKL和MNO節點保留在記憶體中。Ruby不會立即清理我的程式碼不再使用的舊物件!作為Ruby開發人員工作就像生活在一個凌亂的房子裡,衣服躺在地板上或廚房水槽裡的髒盤子。作為Ruby開發人員,您必須使用周圍未使用的垃圾物件。

Python開發人員生活在一個整潔的家庭


Python 
使用它們完成程式碼後立即清理垃圾物件。

垃圾收集在Python中的工作方式與在Ruby中完全不同。讓我們回到之前的三個Python Node物件:

在內部,每當我們建立一個物件時,Python都會在物件的C結構中儲存一個整數,稱為引用計數。最初,Python將此值設定為1:

值1表示對三個物件中的每一個都有一個指標或引用。現在假設我們建立了一個新節點JKL:

與以前一樣,Python將JKL中的引用計數設定為1.但是,由於我們將n1更改為指向JKL,因此它不再引用ABC,並且Python將其引用計數減少為0。

此時,Python垃圾收集器立即開始行動!每當物件的引用計數達到零時,Python立即釋放它,將其記憶體返回給作業系統:

上面的Python回收了ABC節點使用的記憶體。請記住,Ruby只是留下舊物體,並且不釋放它們的記憶。

這種垃圾收集演算法稱為引用計數。它是由喬治·柯林斯(George Collins)在1960年發明的 - 同年約翰·麥卡錫(John McCarthy)發明了自由列表演算法。正如邁克·伯恩斯坦在他夢幻般的說垃圾收集呈現 在世界街頭紅寶石會議在6月份:“1960年對垃圾收集一個好年頭......。”

作為Python開發人員工作就像生活在一個整潔的房子裡; 你知道,你的室友有點強迫症的地方,並在你之後不斷清理。一旦你放下一個髒盤子或玻璃杯,有人已經把它放在洗碗機裡!

現在再來一個例子。假設我們將n2設定為與n1引用相同的節點:

在左上方,您可以看到Python已經減少了DEF的引用計數,並將立即垃圾收集DEF節點。另請注意,JKL現在的引用計數為2,因為n1和 n2都指向它。

標記和掃描

最終,一個混亂的房子充滿了垃圾,生活無法像往常一樣繼續。Ruby程式執行一段時間後,免費列表最終將被完全用完:

這裡所有預先建立的Ruby物件都已被我們的應用程式使用(它們都是灰色的)並且沒有物件保留在空閒列表中(沒有留下白色方塊)。

在這一點上,Ruby使用McCarthy發明的另一種演算法Mark and Sweep。首先Ruby停止你的應用程式; Ruby使用“停止世界垃圾收集。”然後Ruby迴圈遍歷我們的程式碼對物件和其他值的所有指標,變數和其他引用。Ruby還迭代其虛擬機器使用的內部指標。它標記了使用這些指標能夠到達的每個物件。我在這裡用字母M表示這些標記:

標有“M”的三個物件上方是我們的應用程式仍在使用的實時活動物件。在內部,Ruby實際上使用一系列稱為自由點陣圖的位來跟蹤標記的物件:

Ruby將自由點陣圖儲存在單獨的記憶體位置,以便充分利用Unix寫時複製優化。有關這方面的更多資訊,請參閱我的文章為什麼你應該興奮Ruby 2.0中的垃圾收集。

如果標記的物件是活動的,則剩餘的未標記物件必須是垃圾,這意味著我們的程式碼不再使用它們。我將垃圾物件顯示為下方的白色方塊:

Next Ruby 將未使用的垃圾物件回到空閒列表中:

在內部,這種情況很快發生,因為Ruby實際上並沒有將物件從一個地方複製到另一個地方。相反,Ruby通過調整內部指標以形成新的連結串列,將垃圾物件放回到空閒列表中。

現在,Ruby可以在下次建立新的Ruby物件時將這些垃圾物件返回給我們。在Ruby中,物件被轉世,享受多重生命!

標記和掃描與參考計數

乍一看,Python的GC演算法似乎遠遠優於Ruby:為什麼當你可以住在一個整潔的房子裡時,住在一個凌亂的房子裡?為什麼Ruby強制你的應用程式每次清理時都會定期停止執行,而不是使用Python的演算法?

然而,參考計數並不像乍看之下那麼簡單。許多語言不使用像Python這樣的引用計數GC演算法有很多原因:

  • 首先,它很難實現。Python必須在每個物件內留出空間來儲存引用計數。對此有輕微的空間處罰。但更糟糕的是,由於Python需要遞增一個計數器,遞減另一個計數器並可能釋放該物件,因此更改變數或引用這樣的簡單操作會變得更復雜。

  • 其次,它可能會更慢。雖然Python可以在應用程式執行時順利執行GC工作(只要將它們放入接收器中就清理髒盤子),但這並不一定更快。Python不斷更新引用計數值。當您停止使用大型資料結構(例如包含許多元素的列表)時,Python可能必須同時釋放許多物件。減少引用計數可能是一個複雜的遞迴過程。

  • 最後,它並不總是有效。正如我們將在下一篇文章中看到的,其中包含本簡報其餘部分的註釋,引用計數無法處理 迴圈資料結構 - 包含迴圈引用的資料結構。

Python中的迴圈資料結構和引用計數

我們上次看到Python使用儲存在每個物件內部的整數值(稱為引用計數)來跟蹤引用該物件的指標數量。每當程式中的變數或其他物件開始引用物件時,Python就會遞增此計數器; 當程式停止使用物件時,Python會遞減計數器。一旦引用計數變為零,Python就會釋放物件並回收其記憶體。

自20世紀60年代以來,電腦科學家已經意識到這種演算法的一個理論問題:如果你的一個數據結構指的是自身,如果它是一個 迴圈資料結構,一些參考計數永遠不會變為零。為了更好地理解這個問題,我們舉個例子。下面的程式碼顯示了我們上週使用的相同Node類:

我們有一個建構函式(在Python 中稱為__init__),它在例項變數中儲存單個屬性。在類定義下面,我們建立了兩個節點,ABC和DEF,我使用左邊的矩形表示。我們兩個節點內的引用計數最初是一個,因為一個指標(分別為n1和n2)指的是每個節點。

現在讓我們在節點中定義兩個附加屬性,next和prev:

與Ruby不同,使用Python可以像這樣動態定義例項變數或物件屬性。這似乎是Ruby缺少的一些有趣的魔法。(免責宣告:我不是Python開發人員所以我可能會在這裡使用一些錯誤的命名法。)我們將n1.next設定為引用n2,將n2.prev設定 為指向n1。現在,我們的兩個節點使用圓形指標模式形成雙向連結串列。另請注意,ABC和DEF的引用計數已增加到兩個。有兩個指標指向每個節點:n1和n2與之前一樣,現在也是next和 prev。

現在讓我們假設我們的Python程式停止使用節點; 我們將n1和n2都設定為null。(在Python中,null稱為None。)

現在,Python像往常一樣將每個節點內的引用計數減少到1。

Python中的Generation Zero

請注意,在上圖中我們最終得到了一個不尋常的情況:我們有一個“孤島”或一組未使用的物件,這些物件相互引用,但沒有外部引用。換句話說,我們的程式不再使用任何一個節點物件,因此我們希望Python的垃圾收集器足夠智慧以釋放兩個物件併為其他目的回收記憶體。但這不會發生,因為兩個引用計數都是一,而不是零。Python的引用計數演算法無法處理相互引用的物件

當然,這是一個人為的例子,但是你自己的程式可能包含你可能不知道的微妙方式的迴圈引用。事實上,隨著Python程式的執行,它將構建一定數量的“浮動垃圾”,Python收集器無法處理的未使用物件,因為引用計數永遠不會達到零。

這就是Python的代際演算法的用武之地!正如Ruby使用連結串列(自由列表)跟蹤未使用的自由物件一樣,Python使用不同的連結串列來跟蹤活動物件。而不是將其稱為“活動列表”,Python的內部C程式碼將其稱為Generation Zero。每次在程式中建立物件或其他值時,Python都會將其新增到Generation Zero連結列表中

上面你可以看到我們建立ABC節點時,Python將它新增到Generation Zero。請注意,這不是您在程式中看到和訪問的實際列表; 此連結列表完全是Python執行時的內部。

同樣,當我們建立DEF節點時,Python會將其新增到同一個連結串列中:

現在,Generation Zero包含兩個節點物件。(它還將包含我們的Python程式碼建立的所有其他值,以及Python本身使用的許多內部值。)

檢測迴圈參考

稍後Python迴圈遍歷Generation Zero列表中的物件,並檢查列表中每個物件引用的其他物件,隨著時間的推移遞減引用計數。通過這種方式,Python考慮了從一個物件到另一個物件的內部引用,這阻止了Python更早地釋放物件。

為了使這更容易理解,讓我們舉一個例子:

上面你可以看到ABC和DEF節點包含引用計數1.其他三個物件也在Generation Zero連結串列中。藍色箭頭表示某些物件由位於其他位置的其他物件引用 - 來自Generation Zero外部的引用。(正如我們稍後將看到的,Python還使用了另外兩個名為Generation One和Generation Two的列表。)這些物件具有更高的引用計數,因為其他指標指向它們。

下面你可以看到Python的垃圾收集器處理Generation Zero後會發生什麼。

通過識別內部引用,Python可以減少許多Generation Zero物件的引用計數。在頂行的上方,您可以看到ABC和DEF現在的引用計數為零。這意味著收集器將釋放它們並回收它們的記憶。然後將剩餘的活動物件移動到新的連結列表:第一代。

在某種程度上,Python的GC演算法類似於Ruby使用的標記和掃描演算法。它定期跟蹤從一個物件到另一個物件的引用,以確定哪些物件保持活動,我們的程式仍在使用的活動物件 - 就像Ruby的標記過程一樣。

Python中的垃圾收集閾值

Python何時執行此標記過程?當您的Python程式執行時,直譯器會跟蹤它分配的新物件數量,以及由於零引用計數而釋放的物件數量。從理論上講,這兩個值應保持不變:程式建立的每個新物件最終都應該被釋放。

當然,事實並非如此。由於迴圈引用,並且由於程式使用的某些物件比其他物件更長,因此分配計數和釋放計數之間的差異會逐漸增大。一旦此delta值達到某個閾值,就會觸發Python的收集器並使用上面的減法演算法處理Generation Zero列表,釋放“浮動垃圾”並將倖存的物件移動到第一代。

隨著時間的推移,Python程式長時間使用的物件將從Generation Zero列表遷移到Generation One。在分配釋放計數增量值達到甚至更高的閾值之後,Python以類似的方式處理第一代列表上的物件。Python將剩餘的活動物件移動到第二代列表。

通過這種方式,Python程式長時間使用的物件,程式碼保持活動引用,從Generation Zero移動到One到Two。使用不同的閾值,Python以不同的間隔處理這些物件。Python最常處理Generation Zero中的物件,第一代處理頻率較低,而第二代處理頻率較低。

弱代世代假說

這種行為是分代垃圾收集演算法的關鍵:收集器比舊物件更頻繁地處理新物件。新的或 年輕的物件是您的程式剛建立的 物件,而舊的或成熟的物件是在一段時間內保持活動的物件。 當Python 將它從Generation Zero移動到One或從One移動到Two時,Python會提升它。

為什麼這樣?這種演算法背後的基本思想被稱為弱世代假設。這個假設實際上包含兩個觀點:大多數新物體都很年輕,而舊物體很可能長時間保持活躍狀態​​。

假設我使用Python或Ruby建立一個新物件:

根據該假設,我的程式碼可能只在短時間內使用新的ABC節點。該物件可能只是一個方法內部使用的中間值,一旦方法返回就會變為垃圾。大多數新物件都會以這種方式快速變成垃圾。但是,偶爾,我的程式會建立一些物件,這些物件在較長時間內仍然很重要 - 例如Web應用程式中的會話變數或配置值。

通過更頻繁地處理Generation Zero中的新物件,Python的垃圾收集器將大部分時間花在最有利的地方:它處理新物件,這些物件將很快而且經常變成垃圾。只有很少,當分配閾值增加時,Python的收集器會處理舊物件。

六、gc-模組

一、垃圾回收機制

Python中的垃圾回收是以引用計數為主,分代收集為輔。

1、導致引用計數+1的情況

物件被建立,例如a=23

物件被引用,例如b=a

物件被作為引數,傳入到一個函式中,例如func(a)

物件作為一個元素,儲存在容器中,例如list1=[a,a]

2、導致引用計數-1的情況

物件的別名被顯式銷燬,例如del a

物件的別名被賦予新的物件,例如a=24

一個物件離開它的作用域,例如f函式執行完畢時,func函式中的區域性變數(全域性變數不會)

物件所在的容器被銷燬,或從容器中刪除物件

3、檢視一個物件的引用計數

import sys
a = "hello world"
sys.getrefcount(a)

可以檢視a物件的引用計數,但是比正常計數大1,因為呼叫函式的時候傳入a,這會讓a的引用計數+1

二、迴圈引用導致記憶體洩露

引用計數的缺陷是迴圈引用的問題

import gc

class ClassA():
    def __init__(self):
        print('object born,id:%s'%str(hex(id(self))))

def f2():
    while True:
        c1 = ClassA()
        c2 = ClassA()
        c1.t = c2
        c2.t = c1
        del c1
        del c2

#關閉Python中的gc
gc.disable()

f2()

執行f2(),程序佔用的記憶體會不斷增大。

建立了c1,c2後這兩塊記憶體的引用計數都是1,執行 c1.t=c2 和 c2.t=c1 後,這兩塊記憶體的引用計數變成2.

在del c1後,記憶體1的物件的引用計數變為1,由於不是為0,所以記憶體1的物件不會被銷燬,所以記憶體2的物件的引用數依然是2,在del c2後,同理,記憶體1的物件,記憶體2的物件的引用數都是1。

雖然它們兩個的物件都是可以被銷燬的,但是由於迴圈引用,導致垃圾回收器都不會回收它們,所以就會導致記憶體洩露。

三、垃圾回收

#coding=utf-8
import gc
import time

class ClassA():
    def __init__(self):
        print('object born,id:%s'%str(hex(id(self))))
    # def __del__(self):
    #     print('object del,id:%s'%str(hex(id(self))))

def f3():
    print("-----0------")
    # print(gc.collect())
    c1 = ClassA()
    c2 = ClassA()
    c1.t1 = c2
    c2.t2 = c1
    print("-----1------")
    del c1
    del c2
    print(globals())
    print("-----2------")
    print(gc.garbage)
    print("-----3------")
    print(gc.collect()) #顯式執行垃圾回收
    print("-----4------")
    print(gc.garbage)
    print("-----5------")

    time.sleep(0.5)


if __name__ == '__main__':
    # gc.disable()
    gc.set_debug(gc.DEBUG_LEAK) #設定gc模組的日誌
    f3()

執行結果:

-----0------
object born,id:0x724b20
object born,id:0x724b48
-----1------
-----2------
[]
-----3------
gc: collectable <ClassA instance at 0x724b20>
gc: collectable <ClassA instance at 0x724b48>
gc: collectable <dict 0x723300>
gc: collectable <dict 0x71bf60>
4
-----4------
[<__main__.ClassA instance at 0x724b20>, <__main__.ClassA instance 
-----5------

說明:

垃圾回收後的物件會放在gc.garbage列表裡面
gc.collect()會返回不可達的物件數目,4等於兩個物件以及它們對應的dict

有三種情況會觸發垃圾回收:

1. 呼叫gc.collect();
2. 當gc模組的計數器達到閥值的時候;
3. 程式退出的時候;