前面的Erlang雜記中我們簡單提到過Erlang的垃圾回收機制:1.以進程爲單位進行垃圾回收 2.ETS和原子不參與垃圾回收.今天我們繼續這一話題,關注更多關於細節.
在Erlang的官方文檔中,關於垃圾回收的知識散見於各處,要把這些信息收集在一起還是要費些力氣的,完全不像微軟文檔那樣系統化,比如這是關於.net framework垃圾回收的文檔:http://msdn.microsoft.com/en-us/library/ee787088.aspx ;好吧,有點耐心,還是可以從Erlang官方文檔中發現好多寶的,回頭再說,現在開始:
歷史回顧
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參數,這個參數的意思是:最多經過多少代就可以強制進行充分垃圾回收了,不管舊數據堆是否有剩餘空間.
我們看一下這個參數的默認值:
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 Erlang. VM 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