Erlang垃圾回收機制

  前面的Erlang雜記中我們簡單提到過Erlang的垃圾回收機制:1.以進程爲單位進行垃圾回收 2.ETS和原子不參與垃圾回收.今天我們繼續這一話題,關注更多關於細節.

   在Erlang的官方文檔中,關於垃圾回收的知識散見於各處,要把這些信息收集在一起還是要費些力氣的,完全不像微軟文檔那樣系統化,比如這是關於.net framework垃圾回收的文檔:http://msdn.microsoft.com/en-us/library/ee787088.aspx ;好吧,有點耐心,還是可以從Erlang官方文檔中發現好多寶的,回頭再說,現在開始:

 

歷史回顧

 簡單回顧一下垃圾回收的知識,垃圾回收器的本質實際上是改變存活數據結構構成圖的連通性.堆對象在圖中的存活性是由指針的可到達性定義的.程序可以操作三種位置的數據:寄存器 程序棧(局部變量 臨時變量) 全局變量.這些位置的變量有一部分保存了指向堆數據的引用,他們構成了應用程序的根(Root).對於用戶程序動態分配的內存只能通過Root或者根發出的指針鏈訪問,程序不應該訪問其地址空間的隨機位置.
   內存分配的解決方案有:
    [1] 靜態分配 優點是:編譯器知道所有的數據位置,實現效率高 缺點是:每一個數據結構的大小必須在編譯時可知,方法調用不可以是遞歸的,因爲同一個方法在內存中共享相同的位置. 無法動態創建數據結構
    [2] 棧分配 調用的時候入棧,調用結束出棧;同一個方法的不同調用不再共享地址,遞歸成爲可能;只有大小能在編譯時去遞歸大小的對象池啊能作爲過程的結果返回.被調用者的生命週期不可能比調用方的生命週期更長.
    [3]堆分配 閉包成爲可能 遞歸結構的表達式成爲可能
 
   垃圾回收的經典算法有:引用計數 標記清除 節點複製
  [1] 引用計數方法是和程序執行同時進行,內存管理的開銷比較均勻,這樣進行沒有長時間的掛起內存管理的時間比較穩定,可以獲得比較平滑的響應時間;
  [2]標記清除 內存單元不會被立即回收,而是處於不可到達狀態,直到所有的內存都被耗盡,進行全局級別的遍歷來確定哪些單元可以回收.顯然這種全局級別的中斷在實時性要求較高的系統並不實用,甚至視頻遊戲都不可能接受在GC時有這麼長的停頓.如果實時性方面要求不高,標記清除可以獲得比引用計數更好的性能.標記清除的代價還是較高,標記是全局級別的,算法複雜度與整個堆大小成正比;標記清除使得內存空間傾向於碎片化.在物理存儲器中碎片化的影響不大,但虛擬存儲中會導致輔助存儲器和主存之間頻繁的交換頁面,系統出現顛簸.
 [3]節點複製將堆分成兩個半區,一個包含現有數據,另一個包含已經被廢棄的數據,運行時兩個半區的角色不斷交換;這樣做的優勢在於內存分配的開銷很小,只需要比較指針,不存在內存碎片的問題.但是內存浪費較大;
 [4]標記-整理縮並 標記所有的存活對象 通過重新調整存活對象位置來縮並對象圖;更新指向被移動了位置的對象的指針
 [5] 分代回收 是基於統計學原理的:多數內存塊的生存週期都比較短,垃圾收集器應當把更多的精力放在檢查和清理新分配的內存塊上

 IBM公司的David F.Bacon [7] 2004年發表了"A Unified Theory of Garbage Collection"論文,文中闡述了一種理論:任何一種GC算法都是跟蹤回收和引用計數的兩種回收思路的組合;
 

Erlang垃圾回收機制

     "The current default GC is a "stop the world" generational mark-sweep collector. "文檔中這樣描述Erlang垃圾回收器,點出了其垃圾回收器的特點:1."stop the world" 2.generational  3.mark-sweep . 看到這個定義,問題就來了:既然進行垃圾回收的時候會導致進程掛起("stop the world"),那不會影響性能麼?

    先說分代(generational),Erlang使用舊數據堆'old heap'來存儲存至少經歷了一次垃圾回收的數據.當舊數據堆'old heap'沒有足夠的空間的時候就會進行一次充分的垃圾回收(fullsweep).創建進程的時候我們可以通過使用spawn_opt/4來設置fullsweep_after參數,這個參數的意思是:最多經過多少代就可以強制進行充分垃圾回收了,不管舊數據堆是否有剩餘空間.

 分代(generational )本身是基於統計學的:多數內存塊的生存週期都比較短,最近創建的對象更容易變冷(不再被使用).基於上面的考量,Erlang對"年輕"一代對象的GC會更頻繁,減少對常駐內存的對象GC次數.對於一個Erlang進程當沒有足夠Heap空間的時候就會觸發GC.由於Heap是私有的所以進程銷燬的時候內存可以直接回收.Erlang的GC可以分成兩種:minor collection and major collection.
  Minor collection只對年輕一代(young generation)的對象進行GC,Major collection會進行整體GC. 在進行了一定次數Minor Collection後,或者Minor Collection沒能釋放足夠的內存的時候就會觸發Major collection.
 
 

我們看一下這個參數的默認值:
     erlang:system_info(fullsweep_after).   
     {fullsweep_after,65535}

    65535是一個不小的數值,但是現在內存已經不再是稀缺資源,這個值還是可以接受的.如果你希望儘快回收內存的話,這個參數可以適當調整一下.把這個參數設置成0實際上是關閉了數據逐代回收算法,每一次垃圾回收都會拷貝所有livedata.很少有場景需要調整這個值,一般需要調整它的兩個場景是:1.需要快速的拋棄掉不用的二進制數據就把這個值設置爲0; 2.進程使用的數據生命週期都很短,短,舊數據堆會堆積很多垃圾數據;這時可以調小fullsweep_after爲10或20,儘快觸發充分垃圾回收.

   我們啓動Erlang節點,一個節點(node)就是一個Erlang runtime的實例,對應操作系統的一個進程.比如在windows裏面,打開進程管理器會看到erl.exe.在Eralng節點內部動態創建Erlang進程.

     每一個Erlang進程創建之後都會有自己的PCB,棧,私有堆.Erlang進程結束的時候,內存資源理解被釋放便於資源複用.這樣做背後的思想是:每一個進程都只有一小部分活躍數據(live data),所以垃圾回收將會是一個很快的操作.換句話說Erlang的垃圾回收是以進程爲單位的,雖然GC過程會進程掛起但是由於回收速度快,影響很小.垃圾回收使用的是generational stop-and-copy回收器.從Erlang進程終止到其釋放的內存被重用中間是沒有延遲的.由於GC回收是以進程爲單位,垃圾回收器的一個不便之處就是不能跨進程處理進程堆.同樣的,由於進程間數據獨立沒有數據共享,消息發送實際上就是數據複製來實現的,如果複製的數據量很大也是會影響效率的,所以Erlang提倡的是小消息,大運算.

"The basic idea of the Private Heap (PH) architecture is that each process allocates and maintains its own, local, heap. The heap and the stack for each process are allocated in the same memory area and grow towards each other. The main advantage with a scheme like this is that processes’ heaps in general are small which usually makes garbage collection times fairly short.

   The garbage collector is a two generational stop-and-copy. It has two different modes of operation, corresponding to the minor and the major collection. The root set for each garbage collection consists of the process stack, the message queue and an optional vector of pointers sent to the garbage collector.

Note that the garbage collector does not have to scan the stack of any other process. Instead garbage collection happens locally. "

 Erlang Process的ordinary heap存放young generation的數據,歷經2~3次Minor Collection的數據被提升爲old generation,Erlang Process heap空間劃分專門區域存放old generation 數據.Young generation的回收有一個水位線(high water mark)概念,凡是數據地址比水位線要低的都是較老的,比水位線地址高的是更年輕的數據.水位線一下的數據至少經歷了一次minor collection或major collection.

 

 

下面的圖片來自 論文  Characterizing the Scalability of ErlangVM on Many-core Processors


    上面提到進程創建伊始會分配很小的棧和堆資源,這並不是固定不變的,垃圾回收器會動態調整堆大小.Erlang節點創建進程速度超快,這個大家估計已經看過Joe Armstrong在書中創建進程的實驗,這裏不再贅述.那麼一個Erlang進程創建之初到底會佔用多少內存呢?我們可以用下面的例子做一下檢查:

    Fun= fun()-> receive after infinity -> ok end end.   %創建一個無限等待的Fun
    {_,Bytes}=process_info(spawn(Fun),memory).     %創建一個進程並查看其內存信息
    Bytes div erlang:system_info(wordsize).               %計算下這個進程佔用多少字(word) 32系統爲4 64位系統爲8
  

  我們平常用的ETS(Erlang Term Strorage)是一個全局數據庫,可以被節點內的所有進程共享訪問.ETS也是由進程實現,所以存儲和查詢數據和消息發送一樣都是通過複製實現.Erlang二進制數據通常數據量相當大,如果二進制數據<64 bytes會在進程內存儲,如果超過64 bytes二進制數據是在進程以外的獨立的堆分配.二進制數據佔用一塊數據區域,數據區域頭信息包含指向數據區域的指針.當二進制數據分割成子二進制數據段的時候,會創建新的數據頭信息但數據並沒有被拷貝.二進制內存分配對節點內所有的Erlang進程可見.發送消息的時候,二進制數據發送的是引用.如果是跨節點發送二進制數據當然還是通過拷貝實現的.儘管垃圾回收器是基於拷貝的,二進制數據是走的標記-清除(mark-sweep)的路子.我們知道,標記-清除已經是面向全局的垃圾回收機制了.

 

查看垃圾回收狀態

   說到這裏,我們就和上次的內容續上了,[Erlang 0013]抓取Erlang進程運行時信息 提到了如何抓取Erlang進程的運行時信息,這些信息其中也包括了GC的信息:

{garbage_collection, GCInfo}
GCInfo is a list which contains miscellaneous information about garbage collection for this process. The content of GCInfo may be changed without prior notice.

下面是一段採樣數據:

  {reductions,41087},
   {garbage_collection,[{min_bin_vheap_size,10946},
                        {min_heap_size,10946},
                        {fullsweep_after,65535},
                        {minor_gcs,18}]},
   {suspending,[]}]


控制垃圾回收

   我們可以主動控制垃圾回收,使用的方法是erlang:garbage_collect(PID);除此之外我們可以通過調整進程初始堆大小來實現min_heap_size,就是說進程的堆大小不再走自增長的過程,一開始就分配給它足夠的大小.調整這個配置有兩種方法:

    1.erl +h選項可以調整全局的min_heap_size

    2.針對某個進程可以在創建的時候使用spawn_opt/4 來指定min_heap_size 注意:該參數使用的單位是字word
    通過spawn_opt設定進程初始堆大小會有兩個影響:1.進程創建之初就有較大的堆空間,不必經歷自增長的過程 2.即使存儲的數據小於堆大小,垃圾回收時也不再壓縮堆大小;類似的參數還有{min_bin_vheap_size, VSize},可以使用下面的語句查看默認值:

 erlang:system_info(min_heap_size).
{min_heap_size,233}
 erlang:system_info(min_bin_vheap_size).
{min_bin_vheap_size,46368}
  

 何時動手調參數?

什麼時候來調整這些參數呢?你是不是躍躍欲試了?記得一個原則,東西沒有壞的時候不要去修它;用Erlang文檔中反覆出現的一段話來做回答:

This option is only useful for performance tuning. In general, you should not use this option unless you know that there is problem with execution times and/or memory consumption, and you should measure to make sure that the option improved matters.

 

 

 

參考資料

[1] http://www.erlang.org/faq/academic.html 

[2] http://www.erlang.org/doc/man/erlang.html#process_info-2

[3] http://prog21.dadgum.com/16.html

[4] http://www.erlang.org/doc/efficiency_guide/processes.html

[5] http://amiest-devblog.blogspot.com/2008/05/forcing-process-to-garbage-collect-in.html

[6] http://www.lshift.net/blog/2009/12/01/garbage-collection-in-erlang

[7] http://researcher.watson.ibm.com/researcher/files/us-bacon/Bacon04Unified.pdf 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章