日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程语言 > python >内容正文

python

对比Ruby和Python的垃圾回收(2):代式垃圾回收机制

發(fā)布時(shí)間:2023/12/13 python 34 豆豆
生活随笔 收集整理的這篇文章主要介紹了 对比Ruby和Python的垃圾回收(2):代式垃圾回收机制 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.
本文由 伯樂在線 - 熊崽Kevin 翻譯自 patshaughnessy。歡迎加入 技術(shù)翻譯小組。轉(zhuǎn)載請(qǐng)參見文章末尾處的要求。

對(duì)比Ruby和Python的垃圾回收(1)

上周,我根據(jù)之前在RuPy上做的一個(gè)名為“Visualizing Garbage Collection in Ruby and Python.”的報(bào)告寫了這篇文章的上半部分。在上篇中,我解釋了標(biāo)準(zhǔn)Ruby(也被稱為Matz的Ruby解釋器或是MRI)是如何使用名為標(biāo)記回收(Mark and Sweep)的垃圾回收算法,這個(gè)算法是為1960年原版本的Lisp所開發(fā)。同樣,我也介紹了Python使用一種有53年歷史的GC算法,這種算法的思路非常不同,稱之為引用計(jì)數(shù)。

事實(shí)證明,Python在引用計(jì)數(shù)之外,還用了另一個(gè)名為Generational Garbage Collection的算法。這意味著Python的垃圾回收器用不同的方式對(duì)待新創(chuàng)建的以及舊有的對(duì)象。并且在即將到來的2.1版本的MRI Ruby中也首次引入了Generational Garbage Collection 的垃圾回收機(jī)制(在另兩個(gè)Ruby的實(shí)現(xiàn):JRuby和Rubinius中,已經(jīng)使用這種GC機(jī)制很多年了,我將在下周的RubyConf大會(huì)上將它是如何在這兩種Ruby實(shí)現(xiàn)中工作的)。

當(dāng)然,這句話“用不同的方式對(duì)待新創(chuàng)建的以及舊有的對(duì)象”是有點(diǎn)模糊不清,比如如何定義新、舊對(duì)象?又比如對(duì)于Ruby和Python來說具體是如何采取不同的對(duì)待方式?今天,我們就來談?wù)勥@兩種語(yǔ)言GC機(jī)制的運(yùn)行原理,回答上邊那些疑問。但是在我們開始談?wù)揋enerational GC之前,我們先要花點(diǎn)時(shí)間談?wù)撓翽ython的引用計(jì)數(shù)算法的一個(gè)嚴(yán)重的理論問題。

1. Python中的循環(huán)數(shù)據(jù)結(jié)構(gòu)以及引用計(jì)數(shù)

通過上篇,我們知道在Python中,每個(gè)對(duì)象都保存了一個(gè)稱為引用計(jì)數(shù)的整數(shù)值,來追蹤到底有多少引用指向了這個(gè)對(duì)象。無論何時(shí),如果我們程序中的一個(gè)變量或其他對(duì)象引用了目標(biāo)對(duì)象,Python將會(huì)增加這個(gè)計(jì)數(shù)值,而當(dāng)程序停止使用這個(gè)對(duì)象,則Python會(huì)減少這個(gè)計(jì)數(shù)值。一旦計(jì)數(shù)值被減到零,Python將會(huì)釋放這個(gè)對(duì)象以及回收相關(guān)內(nèi)存空間。

從六十年代開始,計(jì)算機(jī)科學(xué)界就面臨了一個(gè)嚴(yán)重的理論問題,那就是針對(duì)引用計(jì)數(shù)這種算法來說,如果一個(gè)數(shù)據(jù)結(jié)構(gòu)引用了它自身,即如果這個(gè)數(shù)據(jù)結(jié)構(gòu)是一個(gè)循環(huán)數(shù)據(jù)結(jié)構(gòu),那么某些引用計(jì)數(shù)值是肯定無法變成零的。為了更好地理解這個(gè)問題,讓我們舉個(gè)例子。下面的代碼展示了一些上周我們所用到的節(jié)點(diǎn)類:

我們有一個(gè)構(gòu)造器(在Python中叫做?init?),在一個(gè)實(shí)例變量中存儲(chǔ)一個(gè)單獨(dú)的屬性。在類定義之后我們創(chuàng)建兩個(gè)節(jié)點(diǎn),ABC以及DEF,在圖中為左邊的矩形框。兩個(gè)節(jié)點(diǎn)的引用計(jì)數(shù)都被初始化為1,因?yàn)楦饔袃蓚€(gè)引用指向各個(gè)節(jié)點(diǎn)(n1和n2)。

現(xiàn)在,讓我們?cè)诠?jié)點(diǎn)中定義兩個(gè)附加的屬性,next以及prev:

?

跟Ruby不同的是,Python中你可以在代碼運(yùn)行的時(shí)候動(dòng)態(tài)定義實(shí)例變量或?qū)ο髮傩浴_@看起來似乎有點(diǎn)像Ruby缺失了某些有趣的魔法。(聲明下我不是一個(gè)Python程序員,所以可能會(huì)存在一些命名方面的錯(cuò)誤)。我們?cè)O(shè)置 n1.next 指向 n2,同時(shí)設(shè)置 n2.prev 指回 n1。現(xiàn)在,我們的兩個(gè)節(jié)點(diǎn)使用循環(huán)引用的方式構(gòu)成了一個(gè)雙端鏈表。同時(shí)請(qǐng)注意到 ABC 以及 DEF 的引用計(jì)數(shù)值已經(jīng)增加到了2。這里有兩個(gè)指針指向了每個(gè)節(jié)點(diǎn):首先是 n1 以及 n2,其次就是 next 以及 prev。

現(xiàn)在,假定我們的程序不再使用這兩個(gè)節(jié)點(diǎn)了,我們將 n1 和 n2 都設(shè)置為null(Python中是None)。

好了,Python會(huì)像往常一樣將每個(gè)節(jié)點(diǎn)的引用計(jì)數(shù)減少到1。

2. 在Python中的零代(Generation Zero)

請(qǐng)注意在以上剛剛說到的例子中,我們以一個(gè)不是很常見的情況結(jié)尾:我們有一個(gè)“孤島”或是一組未使用的、互相指向的對(duì)象,但是誰(shuí)都沒有外部引用。換句話說,我們的程序不再使用這些節(jié)點(diǎn)對(duì)象了,所以我們希望Python的垃圾回收機(jī)制能夠足夠智能去釋放這些對(duì)象并回收它們占用的內(nèi)存空間。但是這不可能,因?yàn)樗械囊糜?jì)數(shù)都是1而不是0。Python的引用計(jì)數(shù)算法不能夠處理互相指向自己的對(duì)象。

當(dāng)然,上邊舉的是一個(gè)故意設(shè)計(jì)的例子,但是你的代碼也許會(huì)在不經(jīng)意間包含循環(huán)引用并且你并未意識(shí)到。事實(shí)上,當(dāng)你的Python程序運(yùn)行的時(shí)候它將會(huì)建立一定數(shù)量的“浮點(diǎn)數(shù)垃圾”,Python的GC不能夠處理未使用的對(duì)象因?yàn)閼?yīng)用計(jì)數(shù)值不會(huì)到零。

這就是為什么Python要引入Generational GC算法的原因!正如Ruby使用一個(gè)鏈表(free list)來持續(xù)追蹤未使用的、自由的對(duì)象一樣,Python使用一種不同的鏈表來持續(xù)追蹤活躍的對(duì)象。而不將其稱之為“活躍列表”,Python的內(nèi)部C代碼將其稱為零代(Generation Zero)。每次當(dāng)你創(chuàng)建一個(gè)對(duì)象或其他什么值的時(shí)候,Python會(huì)將其加入零代鏈表:

從上邊可以看到當(dāng)我們創(chuàng)建ABC節(jié)點(diǎn)的時(shí)候,Python將其加入零代鏈表。請(qǐng)注意到這并不是一個(gè)真正的列表,并不能直接在你的代碼中訪問,事實(shí)上這個(gè)鏈表是一個(gè)完全內(nèi)部的Python運(yùn)行時(shí)。

相似的,當(dāng)我們創(chuàng)建DEF節(jié)點(diǎn)的時(shí)候,Python將其加入同樣的鏈表:

現(xiàn)在零代包含了兩個(gè)節(jié)點(diǎn)對(duì)象。(他還將包含Python創(chuàng)建的每個(gè)其他值,與一些Python自己使用的內(nèi)部值。)

3. 檢測(cè)循環(huán)引用

隨后,Python會(huì)循環(huán)遍歷零代列表上的每個(gè)對(duì)象,檢查列表中每個(gè)互相引用的對(duì)象,根據(jù)規(guī)則減掉其引用計(jì)數(shù)。在這個(gè)過程中,Python會(huì)一個(gè)接一個(gè)的統(tǒng)計(jì)內(nèi)部引用的數(shù)量以防過早地釋放對(duì)象。

為了便于理解,來看一個(gè)例子:

從上面可以看到 ABC 和 DEF 節(jié)點(diǎn)包含的引用數(shù)為1.有三個(gè)其他的對(duì)象同時(shí)存在于零代鏈表中,藍(lán)色的箭頭指示了有一些對(duì)象正在被零代鏈表之外的其他對(duì)象所引用。(接下來我們會(huì)看到,Python中同時(shí)存在另外兩個(gè)分別被稱為一代和二代的鏈表)。這些對(duì)象有著更高的引用計(jì)數(shù)因?yàn)樗鼈冋诒黄渌羔標(biāo)赶蛑?/p>

接下來你會(huì)看到Python的GC是如何處理零代鏈表的。

通過識(shí)別內(nèi)部引用,Python能夠減少許多零代鏈表對(duì)象的引用計(jì)數(shù)。在上圖的第一行中你能夠看見ABC和DEF的引用計(jì)數(shù)已經(jīng)變?yōu)榱懔?#xff0c;這意味著收集器可以釋放它們并回收內(nèi)存空間了。剩下的活躍的對(duì)象則被移動(dòng)到一個(gè)新的鏈表:一代鏈表。

從某種意義上說,Python的GC算法類似于Ruby所用的標(biāo)記回收算法。周期性地從一個(gè)對(duì)象到另一個(gè)對(duì)象追蹤引用以確定對(duì)象是否還是活躍的,正在被程序所使用的,這正類似于Ruby的標(biāo)記過程。

4. Python中的GC閾值

Python什么時(shí)候會(huì)進(jìn)行這個(gè)標(biāo)記過程?隨著你的程序運(yùn)行,Python解釋器保持對(duì)新創(chuàng)建的對(duì)象,以及因?yàn)橐糜?jì)數(shù)為零而被釋放掉的對(duì)象的追蹤。從理論上說,這兩個(gè)值應(yīng)該保持一致,因?yàn)槌绦蛐陆ǖ拿總€(gè)對(duì)象都應(yīng)該最終被釋放掉。

當(dāng)然,事實(shí)并非如此。因?yàn)檠h(huán)引用的原因,并且因?yàn)槟愕某绦蚴褂昧艘恍┍绕渌麑?duì)象存在時(shí)間更長(zhǎng)的對(duì)象,從而被分配對(duì)象的計(jì)數(shù)值與被釋放對(duì)象的計(jì)數(shù)值之間的差異在逐漸增長(zhǎng)。一旦這個(gè)差異累計(jì)超過某個(gè)閾值,則Python的收集機(jī)制就啟動(dòng)了,并且觸發(fā)上邊所說到的零代算法,釋放“浮動(dòng)的垃圾”,并且將剩下的對(duì)象移動(dòng)到一代列表。

隨著時(shí)間的推移,程序所使用的對(duì)象逐漸從零代列表移動(dòng)到一代列表。而Python對(duì)于一代列表中對(duì)象的處理遵循同樣的方法,一旦被分配計(jì)數(shù)值與被釋放計(jì)數(shù)值累計(jì)到達(dá)一定閾值,Python會(huì)將剩下的活躍對(duì)象移動(dòng)到二代列表。

通過這種方法,你的代碼所長(zhǎng)期使用的對(duì)象,那些你的代碼持續(xù)訪問的活躍對(duì)象,會(huì)從零代鏈表轉(zhuǎn)移到一代再轉(zhuǎn)移到二代。通過不同的閾值設(shè)置,Python可以在不同的時(shí)間間隔處理這些對(duì)象。Python處理零代最為頻繁,其次是一代然后才是二代。

5. 弱代假說

來看看代垃圾回收算法的核心行為:垃圾回收器會(huì)更頻繁的處理新對(duì)象。一個(gè)新的對(duì)象即是你的程序剛剛創(chuàng)建的,而一個(gè)來的對(duì)象則是經(jīng)過了幾個(gè)時(shí)間周期之后仍然存在的對(duì)象。Python會(huì)在當(dāng)一個(gè)對(duì)象從零代移動(dòng)到一代,或是從一代移動(dòng)到二代的過程中提升(promote)這個(gè)對(duì)象。

為什么要這么做?這種算法的根源來自于弱代假說(weak generational hypothesis)。這個(gè)假說由兩個(gè)觀點(diǎn)構(gòu)成:首先是年親的對(duì)象通常死得也快,而老對(duì)象則很有可能存活更長(zhǎng)的時(shí)間。

假定現(xiàn)在我用Python或是Ruby創(chuàng)建一個(gè)新對(duì)象:

根據(jù)假說,我的代碼很可能僅僅會(huì)使用ABC很短的時(shí)間。這個(gè)對(duì)象也許僅僅只是一個(gè)方法中的中間結(jié)果,并且隨著方法的返回這個(gè)對(duì)象就將變成垃圾了。大部分的新對(duì)象都是如此般地很快變成垃圾。然而,偶爾程序會(huì)創(chuàng)建一些很重要的,存活時(shí)間比較長(zhǎng)的對(duì)象-例如web應(yīng)用中的session變量或是配置項(xiàng)。

通過頻繁的處理零代鏈表中的新對(duì)象,Python的垃圾收集器將把時(shí)間花在更有意義的地方:它處理那些很快就可能變成垃圾的新對(duì)象。同時(shí)只在很少的時(shí)候,當(dāng)滿足閾值的條件,收集器才回去處理那些老變量。

6. 回到Ruby的自由鏈

即將到來的Ruby 2.1版本將會(huì)首次使用基于代的垃圾回收算法!(請(qǐng)注意的是,其他的Ruby實(shí)現(xiàn),例如JRuby和Rubinius已經(jīng)使用這個(gè)算法許多年了)。讓我們回到上篇博文中提到的自由鏈的圖來看看它到底是怎么工作的。

請(qǐng)回憶當(dāng)自由鏈?zhǔn)褂猛曛?#xff0c;Ruby會(huì)標(biāo)記你的程序仍然在使用的對(duì)象。

從這張圖上我們可以看到有三個(gè)活躍的對(duì)象,因?yàn)橹羔榥1、n2、n3仍然指向著它們。剩下的用白色矩形表示的對(duì)象即是垃圾。(當(dāng)然,實(shí)際情況會(huì)復(fù)雜得多,自由鏈可能會(huì)包含上千個(gè)對(duì)象,并且有復(fù)雜的引用指向關(guān)系,這里的簡(jiǎn)圖只是幫助我們了解Ruby的GC機(jī)制背后的簡(jiǎn)單原理,而不會(huì)將我們陷入細(xì)節(jié)之中)

同樣,我們說過Ruby會(huì)將垃圾對(duì)象移動(dòng)回自由鏈中,這樣的話它們就能在程序申請(qǐng)新對(duì)象的時(shí)候被循環(huán)使用了。

7. Ruby2.1基于代的GC機(jī)制

從2.1版本開始,Ruby的GC代碼增加了一些附加步驟:它將留下來的活躍對(duì)象晉升(promote)到成熟代(mature generation)中。(在MRI的C源碼中使用了old這個(gè)詞而不是mature),接下來的圖展示了兩個(gè)Ruby2.1對(duì)象代的概念圖:

在左邊是一個(gè)跟自由鏈不相同的場(chǎng)景,我們可以看到垃圾對(duì)象是用白色表示的,剩下的是灰色的活躍對(duì)象。灰色的對(duì)象剛剛被標(biāo)記。

一旦“標(biāo)記清除”過程結(jié)束,Ruby2.1將剩下的標(biāo)記對(duì)象移動(dòng)到成熟區(qū):

跟Python中使用三代來劃分不同,Ruby2.1只用了兩代,左邊是年輕的新一代對(duì)象,而右邊是成熟代的老對(duì)象。一旦Ruby2.1標(biāo)記了對(duì)象一次,它就會(huì)被認(rèn)為是成熟的。Ruby會(huì)打賭剩下的活躍對(duì)象在相當(dāng)長(zhǎng)的一段時(shí)間內(nèi)不會(huì)很快變成垃圾對(duì)象。

重要提醒:Ruby2.1并不會(huì)真的在內(nèi)存中拷貝對(duì)象,這些代表不同代的區(qū)域并不是由不同的物理內(nèi)存區(qū)域構(gòu)成。(有一些別的編程語(yǔ)言的GC實(shí)現(xiàn)或是Ruby的其他實(shí)現(xiàn),可能會(huì)在對(duì)象晉升的時(shí)候采取拷貝的操作)。Ruby2.1的內(nèi)部實(shí)現(xiàn)不會(huì)將在標(biāo)記&清除過程中預(yù)先標(biāo)記的對(duì)象包含在內(nèi)。一旦一個(gè)對(duì)象已經(jīng)被標(biāo)記過一次了,那么那將不會(huì)被包含在接下來的標(biāo)記清除過程中。

現(xiàn)在,假定你的Ruby程序接著運(yùn)行著,創(chuàng)造了更多新的,更年輕的對(duì)象。則GC的過程將會(huì)在新的一代中出現(xiàn),如圖:

如同Python那樣,Ruby的垃圾收集器將大部分精力都放在新一代的對(duì)象之上。它僅僅會(huì)將自上一次GC過程發(fā)生后創(chuàng)建的新的、年輕的對(duì)象包含在接下來的標(biāo)記清除過程中。這是因?yàn)楹芏嘈聦?duì)象很可能馬上就會(huì)變成垃圾(白色標(biāo)記)。Ruby不會(huì)重復(fù)標(biāo)記右邊的成熟對(duì)象。因?yàn)樗麄円呀?jīng)在一次GC過程中存活下來了,在相當(dāng)長(zhǎng)的一段時(shí)間內(nèi)不會(huì)很快變成垃圾。因?yàn)橹恍枰獦?biāo)記新對(duì)象,所以Ruby 的GC能夠運(yùn)行得更快。它完全跳過了成熟對(duì)象,減少了代碼等待GC完成的時(shí)間。

偶然的Ruby會(huì)運(yùn)行一次“全局回收”,重標(biāo)記(re-marking)并重清除(re-sweeping),這次包括所有的成熟對(duì)象。Ruby通過監(jiān)控成熟對(duì)象的數(shù)目來確定何時(shí)運(yùn)行全局回收。當(dāng)成熟對(duì)象的數(shù)目雙倍于上次全局回收的數(shù)目時(shí),Ruby會(huì)清理所有的標(biāo)記并且將所有的對(duì)象都視為新對(duì)象。

8. 白障

這個(gè)算法的一個(gè)重要挑戰(zhàn)是值得深入解釋的:假定你的代碼創(chuàng)建了一個(gè)新的年輕的對(duì)象,并且將其作為一個(gè)已存在的成熟對(duì)象的子嗣加入。舉個(gè)例子,這種情況將會(huì)發(fā)生在,當(dāng)你往一個(gè)已經(jīng)存在了很長(zhǎng)時(shí)間的數(shù)組中增加了一個(gè)新值的時(shí)候:

讓我們來看看圖,左邊的是新對(duì)象,而成熟的對(duì)象在右邊。在左邊標(biāo)記過程已經(jīng)識(shí)別出了5個(gè)新的對(duì)象目前仍然是活躍的(灰色)。但有兩個(gè)對(duì)象已經(jīng)變成垃圾了(白色)。但是如何處理正中間這個(gè)新建對(duì)象?這是剛剛那個(gè)問題提到的對(duì)象,它是垃圾還是活躍對(duì)象呢?

當(dāng)然它是活躍對(duì)象了,因?yàn)橛幸粋€(gè)從右邊成熟對(duì)象的引用指向它。但是我們前面說過已經(jīng)被標(biāo)記的成熟對(duì)象是不會(huì)被包含在標(biāo)記清除過程中的(一直到全局回收)。這意味著類似這種的新建對(duì)象會(huì)被錯(cuò)誤的認(rèn)為是垃圾而被釋放,從而造成數(shù)據(jù)丟失。

Ruby2.1 通過監(jiān)視成熟對(duì)象,觀察你的代碼是否會(huì)添加一個(gè)從它們到新建對(duì)象的引用來克服這個(gè)問題。Ruby2.1 使用了一個(gè)名為白障(white barriers)的老式GC技術(shù)來監(jiān)視成熟對(duì)象的變化 – 無論任意時(shí)刻當(dāng)你添加了從一個(gè)對(duì)象指向另一個(gè)對(duì)象的引用時(shí)(無論是新建或是修改一個(gè)對(duì)象),白障就會(huì)被觸發(fā)。白障將會(huì)檢測(cè)是否源對(duì)象是一個(gè)成熟對(duì)象,如果是的話則將這個(gè)成熟對(duì)象添加到一個(gè)特殊的列表中。隨后,Ruby2.1會(huì)將這些滿足條件的成熟對(duì)象包括到下一次標(biāo)記清除的范圍內(nèi),以防止新建對(duì)象被錯(cuò)誤的標(biāo)記為垃圾而清除。

Ruby2.1 的白障實(shí)現(xiàn)相當(dāng)復(fù)雜,主要是因?yàn)橐延械腃擴(kuò)展并未包含這部分功能。Koichi Sasada以及Ruby的核心團(tuán)隊(duì)使用了一個(gè)比較巧妙的方案來解決這個(gè)問題。如果想了解更多的內(nèi)容,請(qǐng)閱讀這些相關(guān)材料:Koichi在EuRuKo 2013上的演講Koichi’s fascinating presentation。

9. 站在巨人的肩膀上

乍眼一看,Ruby和Python的GC實(shí)現(xiàn)是截然不同的,Ruby使用John MaCarthy的原生“標(biāo)記并清除”算法,而Python使用引用計(jì)數(shù)。但是仔細(xì)看來,可以發(fā)現(xiàn)Python使用了些許標(biāo)記清除的思想來處理循環(huán)引用,而兩者同時(shí)以相似的方式使用基于代的垃圾回收算法。Python劃分了三代,而Ruby只有兩代。

這種相似性應(yīng)該不會(huì)讓人感到意外。兩種編程語(yǔ)言都使用了幾十年前的計(jì)算機(jī)科學(xué)研究成果來進(jìn)行設(shè)計(jì),這些成果早在語(yǔ)言成型之前就已經(jīng)被做出來了。我比較驚異的是當(dāng)你掀開不同編程語(yǔ)言的表面而深入底層,你總能夠發(fā)現(xiàn)一些相似的基礎(chǔ)理念和算法。現(xiàn)代編程語(yǔ)言應(yīng)該感激那些六七十年代由麥卡錫等計(jì)算機(jī)先賢所作出的計(jì)算機(jī)科學(xué)開創(chuàng)性研究。

總結(jié)

以上是生活随笔為你收集整理的对比Ruby和Python的垃圾回收(2):代式垃圾回收机制的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。