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

本文由 伯樂在線 - 熊崽Kevin 翻譯自 patshaughnessy。歡迎加入 技術翻譯小組。轉載請參見文章末尾處的要求。

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

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

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

當然,這句話“用不同的方式對待新創建的以及舊有的對象”是有點模糊不清,比如如何定義新、舊對象?又比如對于Ruby和Python來說具體是如何采取不同的對待方式?今天,我們就來談談這兩種語言GC機制的運行原理,回答上邊那些疑問。但是在我們開始談論Generational GC之前,我們先要花點時間談論下Python的引用計數算法的一個嚴重的理論問題。

1. 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。

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

請注意在以上剛剛說到的例子中,我們以一個不是很常見的情況結尾:我們有一個“孤島”或是一組未使用的、互相指向的對象,但是誰都沒有外部引用。換句話說,我們的程序不再使用這些節點對象了,所以我們希望Python的垃圾回收機制能夠足夠智能去釋放這些對象并回收它們占用的內存空間。但是這不可能,因為所有的引用計數都是1而不是0。Python的引用計數算法不能夠處理互相指向自己的對象。

當然,上邊舉的是一個故意設計的例子,但是你的代碼也許會在不經意間包含循環引用并且你并未意識到。事實上,當你的Python程序運行的時候它將會建立一定數量的“浮點數垃圾”,Python的GC不能夠處理未使用的對象因為應用計數值不會到零。

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

從上邊可以看到當我們創建ABC節點的時候,Python將其加入零代鏈表。請注意到這并不是一個真正的列表,并不能直接在你的代碼中訪問,事實上這個鏈表是一個完全內部的Python運行時。

相似的,當我們創建DEF節點的時候,Python將其加入同樣的鏈表:

現在零代包含了兩個節點對象。(他還將包含Python創建的每個其他值,與一些Python自己使用的內部值。)

3. 檢測循環引用

隨后,Python會循環遍歷零代列表上的每個對象,檢查列表中每個互相引用的對象,根據規則減掉其引用計數。在這個過程中,Python會一個接一個的統計內部引用的數量以防過早地釋放對象。

為了便于理解,來看一個例子:

從上面可以看到 ABC 和 DEF 節點包含的引用數為1.有三個其他的對象同時存在于零代鏈表中,藍色的箭頭指示了有一些對象正在被零代鏈表之外的其他對象所引用。(接下來我們會看到,Python中同時存在另外兩個分別被稱為一代和二代的鏈表)。這些對象有著更高的引用計數因為它們正在被其他指針所指向著。

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

通過識別內部引用,Python能夠減少許多零代鏈表對象的引用計數。在上圖的第一行中你能夠看見ABC和DEF的引用計數已經變為零了,這意味著收集器可以釋放它們并回收內存空間了。剩下的活躍的對象則被移動到一個新的鏈表:一代鏈表。

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

4. Python中的GC閾值

Python什么時候會進行這個標記過程?隨著你的程序運行,Python解釋器保持對新創建的對象,以及因為引用計數為零而被釋放掉的對象的追蹤。從理論上說,這兩個值應該保持一致,因為程序新建的每個對象都應該最終被釋放掉。

當然,事實并非如此。因為循環引用的原因,并且因為你的程序使用了一些比其他對象存在時間更長的對象,從而被分配對象的計數值與被釋放對象的計數值之間的差異在逐漸增長。一旦這個差異累計超過某個閾值,則Python的收集機制就啟動了,并且觸發上邊所說到的零代算法,釋放“浮動的垃圾”,并且將剩下的對象移動到一代列表。

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

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

5. 弱代假說

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

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

假定現在我用Python或是Ruby創建一個新對象:

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

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

6. 回到Ruby的自由鏈

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

請回憶當自由鏈使用完之后,Ruby會標記你的程序仍然在使用的對象。

從這張圖上我們可以看到有三個活躍的對象,因為指針n1、n2、n3仍然指向著它們。剩下的用白色矩形表示的對象即是垃圾。(當然,實際情況會復雜得多,自由鏈可能會包含上千個對象,并且有復雜的引用指向關系,這里的簡圖只是幫助我們了解Ruby的GC機制背后的簡單原理,而不會將我們陷入細節之中)

同樣,我們說過Ruby會將垃圾對象移動回自由鏈中,這樣的話它們就能在程序申請新對象的時候被循環使用了。

7. Ruby2.1基于代的GC機制

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

在左邊是一個跟自由鏈不相同的場景,我們可以看到垃圾對象是用白色表示的,剩下的是灰色的活躍對象。灰色的對象剛剛被標記。

一旦“標記清除”過程結束,Ruby2.1將剩下的標記對象移動到成熟區:

跟Python中使用三代來劃分不同,Ruby2.1只用了兩代,左邊是年輕的新一代對象,而右邊是成熟代的老對象。一旦Ruby2.1標記了對象一次,它就會被認為是成熟的。Ruby會打賭剩下的活躍對象在相當長的一段時間內不會很快變成垃圾對象。

重要提醒:Ruby2.1并不會真的在內存中拷貝對象,這些代表不同代的區域并不是由不同的物理內存區域構成。(有一些別的編程語言的GC實現或是Ruby的其他實現,可能會在對象晉升的時候采取拷貝的操作)。Ruby2.1的內部實現不會將在標記&清除過程中預先標記的對象包含在內。一旦一個對象已經被標記過一次了,那么那將不會被包含在接下來的標記清除過程中。

現在,假定你的Ruby程序接著運行著,創造了更多新的,更年輕的對象。則GC的過程將會在新的一代中出現,如圖:

如同Python那樣,Ruby的垃圾收集器將大部分精力都放在新一代的對象之上。它僅僅會將自上一次GC過程發生后創建的新的、年輕的對象包含在接下來的標記清除過程中。這是因為很多新對象很可能馬上就會變成垃圾(白色標記)。Ruby不會重復標記右邊的成熟對象。因為他們已經在一次GC過程中存活下來了,在相當長的一段時間內不會很快變成垃圾。因為只需要標記新對象,所以Ruby 的GC能夠運行得更快。它完全跳過了成熟對象,減少了代碼等待GC完成的時間。

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

8. 白障

這個算法的一個重要挑戰是值得深入解釋的:假定你的代碼創建了一個新的年輕的對象,并且將其作為一個已存在的成熟對象的子嗣加入。舉個例子,這種情況將會發生在,當你往一個已經存在了很長時間的數組中增加了一個新值的時候:

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

當然它是活躍對象了,因為有一個從右邊成熟對象的引用指向它。但是我們前面說過已經被標記的成熟對象是不會被包含在標記清除過程中的(一直到全局回收)。這意味著類似這種的新建對象會被錯誤的認為是垃圾而被釋放,從而造成數據丟失。

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

Ruby2.1 的白障實現相當復雜,主要是因為已有的C擴展并未包含這部分功能。Koichi Sasada以及Ruby的核心團隊使用了一個比較巧妙的方案來解決這個問題。如果想了解更多的內容,請閱讀這些相關材料:Koichi在EuRuKo 2013上的演講Koichi’s fascinating presentation。

9. 站在巨人的肩膀上

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

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

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/447486.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/447486.shtml
英文地址,請注明出處:http://en.pswp.cn/news/447486.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

@Deprecated 注解 (@Documented?、@Retention、@Target)

前些天發現了一個巨牛的人工智能學習網站,通俗易懂,風趣幽默,忍不住分享一下給大家。點擊跳轉到教程。 // 在看 Unsafe 類源碼時看到一個注解:Deprecated,似曾相識... Deprecated 用在類或者方法上,表示…

C++的未來和指針

本文由 伯樂在線 - 周昌鴻 翻譯自 Meeting C。歡迎加入 技術翻譯小組。轉載請參見文章末尾處的要求。上周Meeting C2013結束后,我對C思考了很多,有一些內容和指針有關。在C 11中只對指針進行了小量的更新(引入了nullptr)&#xf…

Java魔法類:Unsafe應用解析

Unsafe是位于sun.misc包下的一個類,主要提供一些用于執行低級別、不安全操作的方法,如直接訪問系統內存資源、自主管理內存資源等,這些方法在提升Java運行效率、增強Java語言底層資源操作能力方面起到了很大的作用。但由于Unsafe類使Java語言…

AMD迎接變革:加速OpenCL的未來

摘要:AMD在北京中關村皇冠假日酒店舉辦了以"迎接變革:加速進入OpenCL 的未來"為主題的技術培訓。AMD Firepro顯卡資深產品經理JC、OpenCL資深講師陸教授、謝博士與大家探討OpenCL技術將如何引領變革、鑄造計算新紀元。 4月11日,AM…

JAVA中神奇的雙刃劍--Unsafe

參考資料: 前些天發現了一個巨牛的人工智能學習網站,通俗易懂,風趣幽默,忍不住分享一下給大家。點擊跳轉到教程。 Java魔法類:sun.misc.Unsafe在openjdk8下看Unsafe源碼 Unsafe介紹 在Oracle的Jdk8無法獲取到sun.misc…

讓AMD在中國發聲 APU14技術創新大會首次在華召開

今日,AMD一年一度的開發者峰會“APU2014”在北京拉開帷幕,這也是AMD首次在美國之外的城市舉辦該活動。AMD全球副總裁、大中華區董事總經理潘曉明表示,大中華區是AMD重要的戰略區域,AMD希望通過本次活動在中國制造巨大的聲音&#…

Python已成美國頂尖高校中最受歡迎的入門編程語言

在最近的一份調查中顯示,美國top高校中,Python已經成為教授計算機科學入門課程方面最受歡迎的語言。其中Top10 CS系中有8所使用Python,Top39 CS系中有24所,在入門課程中教授Python,可見其實用性的認可度很高。在我寫下…

源碼閱讀 AtomicInteger

前些天發現了一個巨牛的人工智能學習網站,通俗易懂,風趣幽默,忍不住分享一下給大家。點擊跳轉到教程。 AtomicInteger 原子整數 可以原子更新的int值。 用于原子遞增計數器等應用程序中,不能用作java.lang.Integer的替換。 擴展…

A飯福利,AMD Mantle API獲眾多游戲開發商青睞!

摘要:Videocardz整理了一份2014年—2015年支持AMD Mantle游戲列表,并公布了游戲開發商及游戲引擎的名稱。已發布且支持Mantle的游戲主要有《戰地4》、《神偷4》、《植物大戰僵尸:花園戰爭》以及《狙擊精英3》這四款。 現如今,越來…

linux 安裝 maven 、解決:bash: mvn: command not found

前些天發現了一個巨牛的人工智能學習網站,通俗易懂,風趣幽默,忍不住分享一下給大家。點擊跳轉到教程。 1、安裝 wget 命令: yum -y install wget 2、下載maven安裝包 wget http://mirrors.cnnic.cn/apache/maven/maven-3/3.5.4/binaries/a…

軟件工程師必學的9件事

本文是html5tricks原創翻譯,轉載請看清文末的轉載要求,謝謝合作! 三年前,我還在巴塞羅那的神經科學實驗室工作,忙著研究腦電波、教授心理學上的認知系統課程。而今天,我以設計和寫軟件為生。 你或許會滿頭…

Linux 的 chmod 命令,對一個目錄及其子目錄所有文件添加權限

前些天發現了一個巨牛的人工智能學習網站,通俗易懂,風趣幽默,忍不住分享一下給大家。點擊跳轉到教程。 對一個目錄及其子目錄所有文件添加權限 命令: chmod 777 -R ./html 給予html目錄下可讀可寫可操作權限。 或者 chmod -R…

Linux 下壓縮與解壓.zip 和 .rar

1)對于.ziplinux下提供了zip和unzip程序,zip是壓縮程序,unzip是解壓程序。它們的參數選項很多,可用命令zip -help和unzip -help查看,這里只做簡單介紹,舉例說明一下其用法:# zip test.zip test.jpg test.pn…

優秀的程序員VS糟糕的程序員

優秀的程序員和一般的程序員差別在哪里?怎么才能成為優秀的程序員?我們選擇了這個職業就要把他做好! 優秀的程序員: 1、邏輯能力很強,這也是解決問題的關鍵。 2、分析能力。可以很好的解決復雜問題。 3、事情做得專…

圖解 Java 常用數據結構

前些天發現了一個巨牛的人工智能學習網站,通俗易懂,風趣幽默,忍不住分享一下給大家。點擊跳轉到教程。 最近在整理數據結構方面的知識, 系統化看了下Java中常用數據結構, 突發奇想用動畫來繪制數據流轉過程. 主要基于jdk8, 可能會有些特性與…

程序員生存定律--使人生永動的勢能

程序員生存定律這系列的目錄在這里:程序員生存定律--目錄 喜歡從頭瞄的,可以移步。 ------------------------------------------------------------------------------- 這篇說的是精神,比較務虛,不感興趣的可以略過。 在國內有…

int 和 Integer 的區別

前些天發現了一個巨牛的人工智能學習網站,通俗易懂,風趣幽默,忍不住分享一下給大家。點擊跳轉到教程。 1、Integer是int的包裝類,int則是java的一種基本數據類型 2、Integer變量必須實例化后才能使用,而int變量不需要…

度量術語之二:應用類和開發類生產率(實際度量案例)

一個令人震驚的事實是連生產率這種常見度量數據都沒有一個簡單的定義。連我們日常經常用到的公式:生產率工作產品/工作量(工作產品可以是代碼行,功能點,也可以是任何可以計數的東西,比如文檔頁數)都是錯誤的…

注解 @ModelAttribute 運用詳細介紹

前些天發現了一個巨牛的人工智能學習網站,通俗易懂,風趣幽默,忍不住分享一下給大家。點擊跳轉到教程。1.ModelAttribute注釋方法   例子(1),(2),(3&#x…

編程語言 IDE 對比

IDE是集成開發環境的英文縮寫,所謂集成開發環境,就是將你在開發過程中所需要的工具或功能集成到了一起,比如代碼編寫、分析、編譯、調試等功能,從而最大化地提高開發者的工作效率。每種編程語言都有一些特定的IDE,本文…