參考鏈接:
https://www.cnblogs.com/alexzhang92/p/9416692.html
https://www.cnblogs.com/neillee/p/6259590.html
目錄
一、基本知識
二、GC垃圾回收
1、引用計數
引用計數的增減
引用計數法有很明顯的優點:
引用計數機制的缺點:
2、畫說 Ruby 與 Python 垃圾回收
2.1 應用程序那顆躍動的心
一個簡單的例子
免費清單
在Python中分配對象
Ruby開發人員住在凌亂的房子里
Python開發人員生活在一個整潔的家庭
標記和掃描
標記和掃描與參考計數
三、Python和Ruby中的分代GC
Python中的循環數據結構和引用計數
Python中的Generation Zero
檢測循環參考
Python中的垃圾收集閾值
一、基本知識
- 小整數[-5,257)共用對象,常駐內存
- 單個字符共用對象,常駐內存
- 單個單詞,不可修改,默認開啟intern機制,共用對象,引用計數為0,則銷毀?
- 字符串(含有空格),不可修改,沒開啟intern機制,不共用對象,引用計數為0,銷毀?
- 大整數不共用內存,引用計數為0,銷毀?
- 數值類型和字符串類型在 Python 中都是不可變的,這意味著你無法修改這個對象的值,每次對變量的修改,實際上是創建一個新的對象?
二、GC垃圾回收
Python采用的是 引用計數機制為主 , 標記-清除和分代收集 兩種機制為輔的策略
1、引用計數
要保持追蹤內存中的對象,Python使用了引用計數這一簡單的技術。
sys.getrefcount(a) 可以查看a對象的引用計數,但是比正常計數大1,因為調用函數的時候傳入a,這會讓a的引用計數+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時,該對象生命就結束了。
引用計數的增減
1、增加引用計數
當對象被創建并(將其引用)賦值給變量時,該對象的引用計數被設置為1。
對象的引用計數增加的情況:
- 對象被創建:x = 3.14
- 另外的別名被創建:y = x
- 對象被作為參數傳遞給函數(新的本地引用):foobar(x)
- 對象成為容器對象的一個元素:myList = [123, x, 'xyz']
2、減少引用計數
對象的引用計數減少的情況:
- 一個本地引用離開了其作用范圍。如fooc()函數結束時,func函數中的局部變量(全局變量不會)
- 對象的別名被顯式銷毀:del y
- 對象的一個別名被賦值給其他對象:x = 123
- 對象被從一個窗口對象中移除:myList.remove(x)
- 窗口對象本身被銷毀:del myList
3、del語句
Del語句會刪除對象的一個引用,它的語法如下:del obj[, obj2[, ...objN]]
例如,在上例中執行del y會產生兩個結果:
- 從現在的名稱空間中刪除y
- x的引用計數減1
引用計數法有很明顯的優點:
- 高效
- 運行期沒有停頓,也就是實時性:一旦沒有引用,內存就直接釋放了。同時把處理回收內存的時間分攤到了平時。
- 對象有確定的生命周期
- 易于實現
引用計數機制的缺點:
- 維護引用計數消耗資源
- 循環引用
list1 = []
list2 = []
list1.append(list2)
list2.append(list1)
list1與list2相互引用,如果不存在其他對象對它們的引用,list1與list2的引用計數也仍然為1,所占用的內存永遠無法被回收,這將是致命的。 對于如今的強大硬件,缺點1尚可接受,但是循環引用導致內存泄露,注定python還將引入新的回收機制。(標記清除和分代收集)
2、畫說 Ruby 與 Python 垃圾回收
英文原文:
http://patshaughnessy.net/2013/10/24/visualizing-garbage-collection-in-ruby-and-python
http://patshaughnessy.net/2013/10/30/generational-gc-in-python-and-ruby
?
2.1 應用程序那顆躍動的心
GC系統所承擔的工作遠比"垃圾回收"多得多。實際上,它們負責三個重要任務。它們
-
為新生成的對象分配內存
-
識別那些垃圾對象,并且
-
從垃圾對象那回收內存。
想象一下,如果您的應用程序是一個人體:您編寫的所有優雅代碼,您的業務邏輯,您的算法,將是應用程序內部的大腦或智能。 按照這個比喻,你認為垃圾收集器的身體部位是什么? [我從RuPy觀眾那里得到了很多有趣的答案:腎臟,白細胞:)]
我認為垃圾收集器是您應用程序的跳動核心。 正如您的心臟為身體的其他部分提供血液和營養,垃圾收集器為您的應用程序提供內存和對象。 如果你的心臟停止跳動,你會在幾秒鐘內死亡。 如果垃圾收集器停止或運行緩慢 - 如果它堵塞了動脈 - 你的應用程序將減速并最終死亡!
?
一個簡單的例子
使用示例來理解理論總是有幫助的。這是一個用Python和Ruby編寫的簡單類,我們今天可以用它作為例子:
免費清單
當我們調用上面的Node.new(1)時,Ruby究竟做了什么?Ruby如何為我們創建一個新對象?
令人驚訝的是,它確實很少!實際上,在您的代碼開始運行之前很久,Ruby就會提前創建數千個對象并將它們放在一個名為 空閑列表的鏈表上 。從概念上講,這是免費列表的樣子:
想象一下,上面的每個白色方塊都是一個未使用的,預先創建的Ruby對象。當我們調用Node.new時,Ruby只需將其中一個對象交給我們:
在上圖中,左側的灰色方塊表示我們在代碼中使用的活動Ruby對象,而剩余的白色方塊是未使用的對象。[注意:當然,我的圖表是現實的簡化版本。事實上,Ruby會使用另一個對象來保存字符串“ABC”,第三個對象用于保存Node的類定義,還有其他對象用于保存我的代碼的解析后的抽象語法樹(AST)表示等。
如果我們再次調用Node.new,Ruby只會給我們另一個對象:
在Python中分配對象
我們已經看到Ruby提前創建對象并將它們保存在空閑列表中。那Python怎么樣?
雖然Python在內部也出于各種原因使用空閑列表(它會回收某些對象,例如列表),但它通常會為 新對象和值分配內存 ,而不像Ruby那樣。
假設我們使用Python創建一個Node對象:
與Ruby不同, Python會在您創建對象時立即向操作系統詢問內存 。(Python實際上實現了自己的內存分配系統,它在操作系統堆之上提供了額外的抽象層。但是我今天沒有時間進入這些細節。)
當我們創建第二個對象時, Python將再次向操作系統詢問更多內存 :
看似簡單;?在我們創建對象的那一刻,Python花時間為我們查找和分配內存。
?
Ruby開發人員住在凌亂的房子里
?
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? Ruby將未使用的對象留在內存中,直到下一個GC進程運行。
回到Ruby。隨著我們分配越來越多的對象,Ruby將繼續從自由列表中提取預先創建的對象。在這樣做時,免費列表將變短:
......而且更短:
請注意,當我繼續為n1分配新值時,Ruby?會將舊值留下。ABC,JKL和MNO節點保留在內存中。Ruby不會立即清理我的代碼不再使用的舊對象!作為Ruby開發人員工作就像生活在一個凌亂的房子里,衣服躺在地板上或廚房水槽里的臟盤子。作為Ruby開發人員,您必須使用周圍未使用的垃圾對象。
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使用麥卡錫發明的另一種算法 Mark and Sweep 。首先Ruby停止你的應用程序;?Ruby使用“停止世界垃圾收集。”然后Ruby循環遍歷我們的代碼對對象和其他值的所有指針,變量和其他引用。Ruby還迭代其虛擬機使用的內部指針。它使用這些指針 標記 它能夠到達的每個對象。我在這里用字母M表示這些標記:
標有“M”的三個對象上方是我們的應用程序仍在使用的實時活動對象。在內部,Ruby實際上使用一系列稱為 自由位圖的位 來跟蹤標記的對象:
Ruby將自由位圖保存在單獨的內存位置,以便充分利用Unix寫時復制優化。
如果標記的對象是活動的,則剩余的未標記對象必須是垃圾,這意味著我們的代碼不再使用它們。我將垃圾對象顯示為下方的白色方塊:
Next Ruby?將未使用的垃圾對象 掃 回到空閑列表中:
在內部,這種情況很快發生,因為Ruby實際上并沒有將對象從一個地方復制到另一個地方。相反,Ruby通過調整內部指針以形成新的鏈表,將垃圾對象放回到空閑列表中。
現在,Ruby可以在下次創建新的Ruby對象時將這些垃圾對象返回給我們。在Ruby中,對象被轉世,享受多重生命!
標記和掃描與參考計數
乍一看,Python的GC算法似乎遠遠優于Ruby:為什么當你可以住在一個整潔的房子里時,住在一個凌亂的房子里?為什么Ruby強制你的應用程序每次清理時都會定期停止運行,而不是使用Python的算法?
然而,參考計數并不像乍一看那么簡單。許多語言不使用像Python這樣的引用計數GC算法有很多原因:
-
首先,它很難實施。Python必須在每個對象內留出空間來保存引用計數。對此有一個小的空間懲罰。但更糟糕的是,改變變量或引用這樣的簡單操作變得更復雜,因為Python需要遞增一個計數器,遞減另一個計數器,并可能釋放該對象。
-
其次,它可能會更慢。雖然Python在應用程序運行時可以順利地執行GC工作(一旦將它們放入接收器中就清理臟盤子),但這不一定更快。Python不斷更新引用計數值。當您停止使用大型數據結構(例如包含許多元素的列表)時,Python可能必須同時釋放多個對象。減少引用計數可能是一個復雜的遞歸過程。
-
最后,它并不總是有效。正如我們將在下一篇文章中看到的,其中包含本演示文稿其余部分的注釋,引用計數無法處理? 循環數據結構 ?- 包含循環引用的數據結構。
三、Python和Ruby中的分代GC
?
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中的對象,第一代處理頻率較低,而第二代甚至更少。
GC模塊的自動垃圾回收觸發機制
參考鏈接:https://blog.csdn.net/u014745194/article/details/70506761
在Python中,采用分代收集的方法。把對象分為三代,一開始,對象在創建的時候,放在0代中,如果在一次0代的垃圾檢查中,改對象存活下來,就會被放到一代中,同理在一次一代的垃圾檢查中,該對象存活下來,就會被放到二代中。
gc模塊里面會有一個長度為3的列表的計數器,可以通過gc.get_count()獲取當前閥值。
例如(666,6,0),其中666是指距離上一次0代垃圾檢查,Python分配內存的數目減去釋放內存的數目,注意是內存分配,而不是引用計數的增加;?
66是指距離上一次一代垃圾檢查,0代垃圾檢查的次數;?
同理,0是指距離上一次二代垃圾檢查,一代垃圾檢查的次數。 ?
案例:
gc模快有一個自動垃圾回收的閥值,即通過gc.get_threshold函數獲取到的長度為3的元組(700,10,10)。每一次計數器的增加,gc模塊就會檢查增加后的計數是否達到閥值的數目,如果是,就會執行對應的代數的垃圾檢查,然后重置計數器
例如,假設閥值是(700,10,10):
1,當計數器從(699,6,0)增加到(700,6,0),gc模塊就會執行gc.collect(0),即檢查0代對象的垃圾,并重置計數器為(0,7,0)
2,當計數器從(699,9,0)增加到(700,9,0),gc模塊就會執行gc.collect(1),即檢查0、一代對象的垃圾,并重置計數器為(0,0,1)
3,當計數器從(699,9,9)增加到(700,9,9),gc模塊就會執行gc.collect(2),即檢查0、一、二代對象的垃圾,并重置計數器為(0,0,0)
?
什么時候觸發垃圾回收機制
1、當gc模塊的計數器達到閥值的時候,自動回收垃圾。?
2、手動調用gc.collect(),手動回收垃圾?
3、程序退出的時候,Python解釋器會回收垃圾。
?
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
