GC垃圾回收在python中是很重要的一部分,一樣我將分兩次去講解Garbage collection垃圾回收,此篇爲Garbage collection垃圾回收第一篇,下面開始今天的說明~~~java
如今的⾼級語⾔如java,c#等,都採⽤了垃圾收集機制,⽽再也不是c,c++⾥ ⽤戶⾃⼰管理維護內存的⽅式。⾃⼰管理內存極其⾃由,能夠任意申請內存,但如同⼀把雙刃劍,爲⼤量內存泄露,懸空指針等bug埋下隱患。 對於⼀個字符串、列表、類甚⾄數值都是對象,且定位簡單易⽤的語⾔,⾃然不會讓⽤戶去處理如何分配回收內存的問題。 python⾥也同java⼀樣採⽤了垃圾收集機制,不過不⼀樣的是: python採⽤的是引⽤計數機制爲主,標記-清除和分代收集兩種機制爲輔的策略。
引⽤計數機制:
python⾥每⼀個東⻄都是對象,它們的核⼼就是⼀個結構體: PyObjectpython
typedef struct_object{ int ob_refcnt; struct_typeobject *ob_type; }PyObject;
PyObject是每一個對象必有的內容,其中ob_refcnt就是作爲引⽤計數。當⼀個對象有新的引⽤時,它的ob_refcnt就會增長,當引⽤它的對象被刪除,它的 ob_refcnt就會減小.c++
#define Py_INCREF(op) ((op)->ob_refcnt++) //增長計數 #defien Py_DECREF(op) \ //減小計數 if(--(op)->ob_refcnt!=0)\ ;\ else \ _Py_Dealloc((PyObject *)(op))
當引⽤計數爲0時,該對象⽣命就結束了。程序員
引⽤計數機制的優勢:web
實時性:⼀旦沒有引⽤,內存就直接釋放了。不⽤像其餘機制等到特定時機。實時性還帶來⼀個好處:處理回收內存的時間分攤到了平時。算法
引⽤計數機制的缺點:編程
循環引⽤c#
list1 = [] list2 = [] list1.append(list2) list2.append(list1)
list1與list2相互引⽤,若是不存在其餘對象對它們的引⽤,list1與list2的引⽤ 計數也仍然爲1,所佔⽤的內存永遠⽆法被回收,這將是致命的。 對於現在的強⼤硬件,缺點1尚可接受,可是循環引⽤致使內存泄露,註定python還將 引⼊新的回收機制。(標記清除和分代收集)ruby
英⽂原⽂: visualizing garbage collection in ruby and pythonsession
GC系統所承擔的⼯做遠⽐"垃圾回收"多得多。實際上,它們負責三個重要任務:
若是將應⽤程序⽐做⼈的身體:全部你所寫的那些優雅的代碼,業務邏輯, 算法,應該就是⼤腦。以此類推,垃圾回收機制應該是那個身體器官呢?(我從RuPy聽衆那聽到了很多有趣的答案:腰⼦、⽩⾎球)
我認爲垃圾回收就是應⽤程序那顆躍動的⼼。像⼼髒爲身體其餘器官提供⾎ 液和養分物那樣,垃圾回收器爲你的應該程序提供內存和對象。若是⼼髒停跳,過不了⼏秒鐘⼈就完了。若是垃圾回收器停⽌⼯做或運⾏遲緩,像動脈阻塞,你的應⽤程序效率也會降低,直⾄最終死掉。
運⽤實例⼀貫有助於理論的理解。下⾯是⼀個簡單類,分別⽤Python和Ruby寫成,咱們今天就以此爲例:
順便提⼀句,兩種語⾔的代碼竟能如此相像:Ruby和Python在表達同⼀事物上真的只是略有不一樣。可是在這兩種語⾔的內部實現上是否也如此類似呢?
當咱們執⾏上⾯的Node.new(1)時,Ruby到底作了什麼?Ruby是如何爲咱們 建立新的對象的呢? 出乎意料的是它作的⾮常少。實際上,早在代碼開始執⾏前,Ruby就提早建立了成百上千個對象,並把它們串在鏈表上,名⽈:可⽤列表。下圖所示爲可⽤列表的概念圖:
想象⼀下每一個⽩⾊⽅格上都標着⼀個"未使⽤預建立對象"。當咱們調⽤ Node.new ,Ruby只需取⼀個預建立對象給咱們使⽤便可:
上圖中左側灰格表示咱們代碼中使⽤的當前對象,同時其餘⽩格是未使⽤對象。(請注意:⽆疑個人示意圖是對實際的簡化。實際上,Ruby會⽤另⼀個 對象來裝載字符串"ABC",另⼀個對象裝載Node類定義,還有⼀個對象裝載了代碼中分析出的抽象語法樹,等等)
若是咱們再次調⽤ Node.new,Ruby將遞給咱們另⼀個對象:
這個簡單的⽤鏈表來預分配對象的算法已經發明瞭超過50年,⽽發明⼈這是 赫赫有名的計算機科學家John McCarthy,⼀開始是⽤Lisp實現的。Lisp不只 是最先的函數式編程語⾔,在計算機科學領域也有許多創舉。其⼀就是利⽤垃圾回收機制⾃動化進⾏程序內存管理的概念。
標準版的Ruby,也就是衆所周知的"Matz's Ruby Interpreter"(MRI),所使⽤的 GC算法與McCarthy在1960年的實現⽅式很相似。⽆論好壞,Ruby的垃圾回 收機制已經53歲⾼齡了。像Lisp⼀樣,Ruby預先建立⼀些對象,而後在你分配新對象或者變量的時候供你使⽤。
咱們已經瞭解了Ruby預先建立對象並將它們存放在可⽤列表中。那Python⼜怎麼樣呢?
儘管因爲許多緣由Python也使⽤可⽤列表(⽤來回收⼀些特定對象⽐如list), 但在爲新對象和變量分配內存的⽅⾯Python和Ruby是不一樣的。
例如咱們⽤Pyhon來建立⼀個Node對象:
與Ruby不一樣,當建立對象時Python⽴即向操做系統請求內存.(Python實際 上實現了⼀套⾃⼰的內存分配系統,在操做系統堆之上提供了⼀個抽象層。 可是我今天不展開說了)
當咱們建立第⼆個對象的時候,再次像OS請求內存:
看起來夠簡單吧,在咱們建立對象的時候,Python會花些時間爲咱們找到並分配內存。
Ruby把⽆⽤的對象留在內存⾥,直到下⼀次GC執⾏
回過來看Ruby。隨着咱們建立愈來愈多的對象,Ruby會持續尋可⽤列表⾥ 取預建立對象給咱們。所以,可⽤列表會逐漸變短:
...而後更短:
請注意我⼀直在爲變量n1賦新值,Ruby把舊值留在原處。"ABC","JKL"和"MNO"三個Node實例還滯留在內存中。Ruby不會⽴即清除代碼中再也不使⽤的舊對象!Ruby開發者們就像是住在⼀間凌亂的房間,地板上摞着⾐服,要麼洗碗池⾥都是髒盤⼦。做爲⼀個Ruby程序員,⽆⽤的垃圾對象會⼀直環繞着你。
⽤完的垃圾對象會⽴即被Python打掃⼲淨
Python與Ruby的垃圾回收機制頗爲不一樣。讓咱們回到前⾯提到的三個Python Node對象:
在內部,建立⼀個對象時,Python老是在對象的C結構體⾥保存⼀個整數, 稱爲引⽤數 。期初,Python將這個值設置爲1:
值爲1說明分別有個⼀個指針指向或是引⽤這三個對象。假如咱們如今建立⼀個新的Node實例,JKL:
與以前⼀樣,Python設置JKL的引⽤數爲1。然⽽,請注意因爲咱們改變了n1 指向了JKL,再也不指向ABC,Python就把ABC的引⽤數置爲0了。 此刻, Python垃圾回收器⽴刻挺身⽽出!每當對象的引⽤數減爲0,Python⽴即將其釋放,把內存還給操做系統:
上⾯Python回收了ABC Node實例使⽤的內存。記住,Ruby棄舊對象原地於不顧,也不釋放它們的內存。
Python的這種垃圾回收算法被稱爲引⽤計數。是George-Collins在1960年發明的,恰巧與John McCarthy發明的可⽤列表算法在同⼀年出現。就像MikeBernstein在6⽉份哥譚市Ruby⼤會傑出的垃圾回收機制演講中說的: "1960年是垃圾收集器的⻩⾦年代..."。
Python開發者⼯做在衛⽣之家,你能夠想象,有個患有輕度OCD(⼀種強迫症) 的室友⼀刻不停地跟在你身後打掃,你⼀放下髒碟⼦或杯⼦,有個傢伙已經 準備好把它放進洗碗機了!
如今來看第⼆例⼦。加⼊咱們讓n2引⽤n1:
上圖中左邊的DEF的引⽤數已經被Python減小了,垃圾回收器會⽴即回收DEF實例。同時JKL的引⽤數已經變爲了2 ,由於n1和n2都指向它。
最終那間凌亂的房間充斥着垃圾,再不能歲⽉靜好了。在Ruby程序運⾏了⼀陣⼦之後,可⽤列表最終被⽤光光了:
此刻全部Ruby預建立對象都被程序⽤過了(它們都變灰了),可⽤列表⾥空空如也(沒有⽩格⼦了)。
此刻Ruby祭出另⼀McCarthy發明的算法,名⽈:標記-清除。⾸先Ruby把程 序停下來,Ruby⽤"地球停轉垃圾回收⼤法"。以後Ruby輪詢全部指針,變量 和代碼產⽣別的引⽤對象和其餘值。同時Ruby經過⾃身的虛擬機便利內部指針。標記出這些指針引⽤的每一個對象。我在圖中使⽤M表示。
上圖中那三個被標M的對象是程序還在使⽤的。在內部,Ruby實際上使⽤⼀串位值,被稱爲:可⽤位圖(譯註:還記得《編程珠璣》⾥的爲突發排序嗎,這 對離散度不⾼的有限整數集合具備很強的壓縮效果,⽤以節約機器的資源),來跟蹤對象是否被標記了。
若是說被標記的對象是存活的,剩下的未被標記的對象只能是垃圾,這意味 着咱們的代碼再也不會使⽤它了。我會在下圖中⽤⽩格⼦表示垃圾對象:
接下來Ruby清除這些⽆⽤的垃圾對象,把它們送回到可⽤列表中:
在內部這⼀切發⽣得迅雷不及掩⽿,由於Ruby實際上不會吧對象從這拷⻉到 那。⽽是經過調整內部指針,將其指向⼀個新鏈表的⽅式,來將垃圾對象歸位到可⽤列表中的。
如今等到下回再建立對象的時候Ruby⼜能夠把這些垃圾對象分給咱們使⽤了。在Ruby⾥,對象們六道輪迴,轉世投胎,享受屢次⼈⽣。
乍⼀看,Python的GC算法貌似遠勝於Ruby的:寧舍潔宇⽽居穢室乎?爲何Ruby寧願按期強制程序停⽌運⾏,也不使⽤Python的算法呢?
然⽽,引⽤計數並不像第⼀眼看上去那樣簡單。有許多緣由使得不準多語⾔ 不像Python這樣使⽤引⽤計數GC算法:
⾸先,它很差實現。Python不得不在每一個對象內部留⼀些空間來處理引⽤數。這樣付出了⼀⼩點⼉空間上的代價。但更糟糕的是,每一個簡單的操做(像修改變量或引⽤)都會變成⼀個更復雜的操做,由於Python須要增長⼀ 個計數,減小另⼀個,還可能釋放對象。
第⼆點,它相對較慢。雖然Python隨着程序執⾏GC很穩健(⼀把髒碟⼦放在 洗碗盆⾥就開始洗啦),但這並不⼀定更快。Python不停地更新着衆多引⽤ 數值。特別是當你再也不使⽤⼀個⼤數據結構的時候,⽐如⼀個包含不少元素的列表,Python可能必須⼀次性釋放⼤量對象。減小引⽤數就成了⼀項複雜的遞歸過程了。
最後,它不是總奏效的。引⽤計數不能處理環形數據結構--也就是含有循環引⽤的數據結構。
經過上篇,咱們知道在Python中,每一個對象都保存了⼀個稱爲引⽤計數的整數值,來追蹤到底有多少引⽤指向了這個對象。⽆論什麼時候,若是咱們程序中的⼀個變量或其餘對象引⽤了⽬標對象,Python將會增長這個計數值,⽽當 程序停⽌使⽤這個對象,則Python會減小這個計數值。⼀旦計數值被減到 零,Python將會釋放這個對象以及回收相關內存空間。
從六⼗年代開始,計算機科學界就⾯臨了⼀個嚴重的理論問題,那就是針對 引⽤計數這種算法來講,若是⼀個數據結構引⽤了它⾃身,即若是這個數據 結構是⼀個循環數據結構,那麼某些引⽤計數值是確定⽆法變成零的。爲了更好地理解這個問題,讓咱們舉個例⼦。下⾯的代碼展現了⼀些上週咱們所⽤到的節點類:
咱們有⼀個"構造器"(在Python中叫作 __init__ ),在⼀個實例變量中存儲⼀個單獨的屬性。在類定義以後咱們建立兩個節點,ABC以及DEF,在圖中爲左邊的矩形框。兩個節點的引⽤計數都被初始化爲1,由於各有兩個引⽤指向各個節點(n1和n2)。
如今,讓咱們在節點中定義兩個附加的屬性,next以及prev:
跟Ruby不一樣的是,Python中你能夠在代碼運⾏的時候動態定義實例變量或對象屬性。這看起來彷佛有點像Ruby缺失了某些有趣的魔法。(聲明下我不是 ⼀個Python程序員,因此可能會存在⼀些命名⽅⾯的錯誤)。咱們設置 n1.next 指向 n2,同時設置 n2.prev 指回 n1。如今,咱們的兩個節點使⽤循 環引⽤的⽅式構成了⼀個 雙向鏈表 。同時請注意到 ABC 以及 DEF 的引⽤計 數值已經增長到了2。這⾥有兩個指針指向了每一個節點:⾸先是 n1 以及 n2, 其次就是 next 以及 prev。
如今,假定咱們的程序再也不使⽤這兩個節點了,咱們將 n1 和 n2 都設置爲 null(Python中是None)。
好了,Python會像往常⼀樣將每一個節點的引⽤計數減小到1。
請注意在以上剛剛說到的例⼦中,咱們以⼀個不是很常⻅的狀況結尾:咱們 有⼀個「孤島」或是⼀組未使⽤的、互相指向的對象,可是誰都沒有外部引 ⽤。換句話說,咱們的程序再也不使⽤這些節點對象了,因此咱們但願Python 的垃圾回收機制可以⾜夠智能去釋放這些對象並回收它們佔⽤的內存空間。 可是這不可能,由於全部的引⽤計數都是1⽽不是0。Python的引⽤計數算法 不可以處理互相指向⾃⼰的對象。
這就是爲何Python要引⼊ Generational GC 算法的緣由!正如Ruby使⽤ ⼀個鏈表(free list)來持續追蹤未使⽤的、⾃由的對象⼀樣,Python使⽤⼀種 不一樣的鏈表來持續追蹤活躍的對象。⽽不將其稱之爲「活躍列表」,Python的 內部C代碼將其稱爲零代(Generation Zero)。每次當你建立⼀個對象或其餘什麼值的時候,Python會將其加⼊零代鏈表:
從上邊能夠看到當咱們建立ABC節點的時候,Python將其加⼊零代鏈表。請 注意到這並非⼀個真正的列表,並不能直接在你的代碼中訪問,事實上這個鏈表是⼀個徹底內部的Python運⾏時。 類似的,當咱們建立DEF節點的時候,Python將其加⼊一樣的鏈表:
如今零代包含了兩個節點對象。(他還將包含Python建立的每一個其餘值,與⼀些Python⾃⼰使⽤的內部值)
隨後,Python會循環遍歷零代列表上的每一個對象,檢查列表中每一個互相引⽤的對象,根據規則減掉其引⽤計數。在這個過程當中,Python會⼀個接⼀個的 統計內部引⽤的數量以防過早地釋放對象。
爲了便於理解,來看⼀個例⼦:
從上⾯能夠看到 ABC 和 DEF 節點包含的引⽤數爲1.有三個其餘的對象同時 存在於零代鏈表中,藍⾊的箭頭指示了有⼀些對象正在被零代鏈表以外的其餘對象所引⽤。(接下來咱們會看到,Python中同時存在另外兩個分別被稱爲 ⼀代和⼆代的鏈表)。這些對象有着更⾼的引⽤計數由於它們正在被其餘指針所指向着。
接下來你會看到Python的GC是如何處理零代鏈表的。
經過識別內部引⽤,Python可以減小許多零代鏈表對象的引⽤計數。在上圖 的第⼀⾏中你可以看⻅ABC和DEF的引⽤計數已經變爲零了,這意味着收集器能夠釋放它們並回收內存空間了。剩下的活躍的對象則被移動到⼀個新的 鏈表:⼀代鏈表。
從某種意義上說,Python的GC算法相似於Ruby所⽤的標記回收算法。週期性地從⼀個對象到另⼀個對象追蹤引⽤以肯定對象是否仍是活躍的,正在被程序所使⽤的,這正相似於Ruby的標記過程。
Python何時會進⾏這個標記過程?隨着你的程序運⾏,Python解釋器保 持對新建立的對象,以及由於引⽤計數爲零⽽被釋放掉的對象的追蹤。從理論上說,這兩個值應該保持⼀致,由於程序新建的每一個對象都應該最終被釋放掉。
固然,事實並⾮如此。由於循環引⽤的緣由,而且由於你的程序使⽤了⼀些 ⽐其餘對象存在時間更⻓的對象,從⽽被分配對象的計數值與被釋放對象的計數值之間的差別在逐漸增⻓。⼀旦這個差別累計超過某個閾值,則Python的收集機制就啓動了,而且觸發上邊所說到的零代算法,釋放「浮動的垃圾」,而且將剩下的對象移動到⼀代列表。
隨着時間的推移,程序所使⽤的對象逐漸從零代列表移動到⼀代列表。⽽ Python對於⼀代列表中對象的處理遵循一樣的⽅法,⼀旦被分配計數值與被 釋放計數值累計到達⼀定閾值,Python會將剩下的活躍對象移動到⼆代列表。
經過這種⽅法,你的代碼所⻓期使⽤的對象,那些你的代碼持續訪問的活躍 對象,會從零代鏈表轉移到⼀代再轉移到⼆代。經過不一樣的閾值設置, Python能夠在不一樣的時間間隔處理這些對象。Python處理零代最爲頻繁,其 次是⼀代而後纔是⼆代。
弱代假說
來看看代垃圾回收算法的核⼼⾏爲:垃圾回收器會更頻繁的處理新對象。⼀個新的對象便是你的程序剛剛建立的,⽽⼀個來的對象則是通過了⼏個時間 週期以後仍然存在的對象。Python會在當⼀個對象從零代移動到⼀代,或是 從⼀代移動到⼆代的過程當中提高(promote)這個對象。
爲何要這麼作?這種算法的根源來⾃於弱代假說(weak generational hypothesis)。這個假說由兩個觀點構成:⾸先是年親的對象一般死得也快, ⽽⽼對象則頗有可能存活更⻓的時間。
假定如今我⽤Python或是Ruby建立⼀個新對象:
根據假說,個人代碼極可能僅僅會使⽤ABC很短的時間。這個對象也許僅僅 只是⼀個⽅法中的中間結果,而且隨着⽅法的返回這個對象就將變成垃圾了。⼤部分的新對象都是如此般地很快變成垃圾。然⽽,偶爾程序會建立⼀ 些很重要的,存活時間⽐較⻓的對象-例如web應⽤中的session變量或是配置項。經過頻繁的處理零代鏈表中的新對象,Python的垃圾收集器將把時間花在更 有意義的地⽅:它處理那些很快就可能變成垃圾的新對象。同時只在不多的 時候,當滿⾜閾值的條件,收集器纔回去處理那些⽼變量。