聲明:本片文章是由Hackernews上的[Erlang Garbage Collection Details and Why It
Matters][1]編譯而來,本着學習和研究的態度,進行的編譯,轉載請註明出處。html
Erlang須要解決的重要問題之一就是爲實現極高響應能力的軟實時系統建立平臺。這樣的系統須要一個快速的垃圾回收機制,而這個機制不會阻止系統及時的響應。另外一方面,當咱們把Erlang看做一種用無損更新屬性的不可改變語言時,這個垃圾回收機制就顯得更加劇要了,由於這種語言有很高的概率產生垃圾。golang
在深刻了解GC以前,有一個很重要的事,就是檢查Erlang過程的內存佈局的三個重要的點:進程控制模塊,棧和堆。它和Unix的內存佈局很是的相像。
進程控制模塊:進程控制模塊會保存一些關於進程的信息好比它在進程表中的標識符(PID)、當前狀態(運行、等待)、它的註冊名、初始和當前調用,同時PCB也會保存一些指向傳入消息的指針,這些傳入消息是存儲在堆中鏈接表中的。express
棧:它是一個向下生長的存儲區,這個存儲區保存輸入和輸出參數、返回地址、本地變量和用於evaluating expressions的臨時空間。編程
堆:它是一個向上生長的存儲區,這個存儲區保存進程郵箱的物理消息,像列表、元組和Binaries這種的複合項以及比像浮點數這種一個機器字更大的對象。超過64機器字的二進制項不會存儲在進程私有堆裏。他們被稱做Refc Binary (Reference Counted Binary)並被存儲在一個大的共享堆裏,只要有那個Refc Binary指針的進程均可以訪問這個堆。這個儲存在進程私有堆中的指針叫做ProcBin。segmentfault
爲告終實當前默認Erlang的GC機制,簡單的說,它是一個分代複製的垃圾回收,獨立運行在每一個Erlang進程私有堆的內部,並且它也是發生在全球共享堆中的引用計數垃圾回收。安全
私有堆的GC是分代的。分代GC把堆分爲了新生和老年代兩個部分。若是一個對象在GC循環生存下來,那麼它在短時間內成爲垃圾的概率將會很低,這也是這個劃分的依據所在。所以,新生代是爲新分配的數據準備的,老年代是爲了在數次GC啓動後生存下來數據的。這個分代幫助了GC減小在尚未成爲垃圾數據上的沒必要要的循環。對於Erlang垃圾回收有兩個策略:Generational (Minor)和Fullsweep (Major)。分代的GC只收集新生的堆,而fullsweep的堆新老都會收集。如今,讓咱們回顧一個新開始Erlang進程私有堆的GC步驟:ide
場景1:函數
Spawn > No GC > Terminate佈局
若是一個短暫的進程沒有使用超過min_heap_size的堆就結束了,GC是不會發生的。這種狀況下全部被進程使用過的內存會被收集。學習
場景2:
Spawn > Fullsweep > Generational > Terminate
若是一個新生產的進程的數據增加超過min_heap_size,那麼會使用fullsweep GC,顯然這是由於沒有GC發生,那麼也不會有新生代和老年代之分。在第一次fullsweep GC後,堆就會被分代成這兩部分,以後GC策略會轉化到分代並保持到進程結束。
場景3:
Spawn > Fullsweep > Generational > Fullsweep > Generational > ... >
Terminate
有幾種狀況,GC策略在進程過程當中由分代轉化回到fullsweep。第一種狀況是進過必定次數的分代GC。這個數量能夠是特定全局的或者是每一個有fullsweep_after flag的進程。同時在fullsweep GC以前每一個的進程和它的上限的分代GC計數器分別是minor_gcs 和 fullsweep_after特性,並在process_info(PID, garbage_collection)返回值中可見。第二種狀況是當分代GC不能收集到足夠的內存,最後一種狀況是garbage_collect(PID)函數被手動調用。在這些狀況後,GC策略會回覆到從fullsweep到分代而後保持直到上述情形發生。
場景4:
Spawn > Fullsweep > Generational > Fullsweep > Increase Heap >
Fullsweep > ... > Terminate
在場景3中,若是第二fullsweep GC不能收集到足夠內存,堆的大小會增長,GC策略又會轉化成fullsweep,就像新生成的進程同樣,這四種場景能夠不斷的出現。
如今的問題是爲何在像Erlang這種自動垃圾收集語言這麼重要。首先這些知識能夠幫助你經過調整GC的發生和策略使你的系統運行更快。其次,這是咱們明白從垃圾回收的角度使Erlang變成軟件實時平臺的重要緣由的地方。這是由於每一個進程都有它本身的堆和它本身的GC,因此每次GC出如今一個進程中的時候,只是中止正在收集過程當中的Erlang進程,但不會中止其餘的進程,而這正是一個軟實時系統所須要的。
共享堆的GC是參考計數。每一個在共享堆(Refc)的對象都有與存儲的其餘對象(ProcBin)相對的參考計數器,這些其餘對象(ProcBin)都存儲在Erlang進程私有堆內部。若是一個對象參考計數器達到0,這個對象會變得沒法訪問並將銷燬。參考計數器很廉價而且能夠幫助系統避免意外長時的暫停並且提升體統的響應速度。可是在設計你的actor模型系統時,不瞭解一些著名的反模式會致使一些問題,好比內存泄漏。
當Refc第一次分紅一個Sub-Binary。爲了下降成本,一個sub-binary不是一個原binary分裂部分的新副本,僅僅是那個部分的一個參考。然而這個sub-binary會被看成加入到原binary的的一個新的參考,你知道,當原binary必須掛在它的sub-binary上時,這可能會引發一些問題。
其餘已知的問題會發生在當一種生命週期很長的中間件看成控制和傳遞大型Refc binary消息的請求控制器或消息路由器時。當這個進程接觸到每一個Refc消息時,它們的計數器會遞增。所以收集這些Refc消息依靠於收集全部ProcBin對象,即便它們在中間件進程中。不幸的是,由於ProcBin僅僅只是個指針,所以它們成本很低並且在中間件進程中須要花很長的時間去觸發GC。因此即便已經從除了中間件其餘全部進程中收集了Refc消息,它們也須要保留在共享堆裏。
共享堆之因此重要是由於它減小了因爲在進程之間傳遞大量binary消息的IO。因爲sub-binaries僅僅是其餘binary的指針,他們能夠快速的建立。可是做爲一種經驗法則,使用變得更快的捷徑會產生成本,這個成本會以一種不會在惡劣條件下困住方式去構建你的系統。同時也有不少應對Refc binary泄露的著名方法,好比Fred Hebert在他的ebook發表的Erlang in Anger。我認爲我不能解釋的比他更好,因此強烈推薦你去閱讀。
即便咱們使用像Erlang這種自我管理內存的語言,瞭解內存是如何分配和釋放也是很必要的。不像Go的內存模型文檔建議你「若是你必需要經過閱讀剩下的文檔去了解你的編程的行爲,那麼你太聰明瞭。不要這麼聰明」,我相信咱們必需要足夠的聰明去讓咱們的系統運行得更快更安全,但作到這一點,深刻了解它的原理是必不可少的。
• Academic and Historical Questions about Erlang
• Implementation of FPL & Concurrency
• Efficient Memory Management for Message-Passing Concurrency Paper
• Programming the Parallel World by Erlang Paper
關於Erlang內存泄漏的問題的一些分析能夠參見雲巴以前的一篇Erlang內存泄漏分析的文章有什麼問題歡迎留言交流。