golang 垃圾回收機制

用任何帶 GC 的語言最後都要直面 GC 問題。在之前學習 C# 的時候就被迫讀了一大堆 .NET Garbage Collection 的文檔。最近也學習了一番 golang 的垃圾回收機制,在這裏記錄一下。java

 


 

常見 GC 算法

趁着這個機會我總結了一下常見的 GC 算法。分別是:引用計數法、Mark-Sweep法、三色標記法、分代收集法。c++

 

1. 引用計數法

原理是在每一個對象內部維護一個整數值,叫作這個對象的引用計數,當對象被引用時引用計數加一,當對象不被引用時引用計數減一。當引用計數爲 0 時,自動銷燬對象。git

目前引用計數法主要用在 c++ 標準庫的 std::shared_ptr 、微軟的 COM 、Objective-C 和 PHP 中。github

可是引用計數法有個缺陷就是不能解決循環引用的問題。循環引用是指對象 A 和對象 B 互相持有對方的引用。這樣兩個對象的引用計數都不是 0 ,所以永遠不能被收集。golang

另外的缺陷是,每次對象的賦值都要將引用計數加一,增長了消耗。算法

 

2. Mark-Sweep法(標記清除法)

這個算法分爲兩步,標記和清除。併發

  • 標記:從程序的根節點開始, 遞歸地 遍歷全部對象,將能遍歷到的對象打上標記。
  • 清除:講全部未標記的的對象看成垃圾銷燬。

 


Animation_of_the_Naive_Mark_and_Sweep_Garbage_Collector_Algorithm.gif-143.9kB 
圖片來自 https://en.wikipedia.org/wiki/Tracing_garbage_collection 

 

如圖所示。jvm

可是這個算法也有一個缺陷,就是人們經常說的 STW 問題(Stop The World)。由於算法在標記時必須暫停整個程序,不然其餘線程的代碼可能會改變對象狀態,從而可能把不該該回收的對象當作垃圾收集掉。ide

當程序中的對象逐漸增多時,遞歸遍歷整個對象樹會消耗不少的時間,在大型程序中這個時間可能會是毫秒級別的。讓全部的用戶等待幾百毫秒的 GC 時間這是不能容忍的。高併發

golang 1.5之前使用的這個算法。

 

3. 三色標記法

三色標記法是傳統 Mark-Sweep 的一個改進,它是一個併發的 GC 算法。

原理以下,

  1. 首先建立三個集合:白、灰、黑。
  2. 將全部對象放入白色集合中。
  3. 而後從根節點開始遍歷全部對象(注意這裏並不遞歸遍歷),把遍歷到的對象從白色集合放入灰色集合。
  4. 以後遍歷灰色集合,將灰色對象引用的對象從白色集合放入灰色集合,以後將此灰色對象放入黑色集合
  5. 重複 4 直到灰色中無任何對象
  6. 經過write-barrier檢測對象有變化,重複以上操做
  7. 收集全部白色對象(垃圾)

 


Animation_of_tri-color_garbage_collection.gif-94kB 
圖片來自 https://en.wikipedia.org/wiki/Tracing_garbage_collection 

 

過程如上圖所示。

這個算法能夠實現 "on-the-fly",也就是在程序執行的同時進行收集,並不須要暫停整個程序。

可是也會有一個缺陷,可能程序中的垃圾產生的速度會大於垃圾收集的速度,這樣會致使程序中的垃圾愈來愈多沒法被收集掉。

使用這種算法的是 Go 1.五、Go 1.6。

 

4. 分代收集

分代收集也是傳統 Mark-Sweep 的一個改進。這個算法是基於一個經驗:絕大多數對象的生命週期都很短。因此按照對象的生命週期長短來進行分代。

通常 GC 都會分三代,在 java 中稱之爲新生代(Young Generation)、年老代(Tenured Generation)和永久代(Permanent Generation);在 .NET 中稱之爲第 0 代、第 1 代和第2代。

原理以下:

  • 新對象放入第 0 代
  • 當內存用量超過一個較小的閾值時,觸發 0 代收集
  • 第 0 代倖存的對象(未被收集)放入第 1 代
  • 只有當內存用量超過一個較高的閾值時,纔會觸發 1 代收集
  • 2 代同理

由於 0 代中的對象十分少,因此每次收集時遍歷都會很是快(比 1 代收集快幾個數量級)。只有內存消耗過於大的時候纔會觸發較慢的 1 代和 2 代收集。

所以,分代收集是目前比較好的垃圾回收方式。使用的語言(平臺)有 jvm、.NET 。


 

golang 的 GC

go 語言在 1.3 之前,使用的是比較蠢的傳統 Mark-Sweep 算法。

1.3 版本進行了一下改進,把 Sweep 改成了並行操做。

1.5 版本進行了較大改進,使用了三色標記算法。go 1.5 在源碼中的解釋是「非分代的、非移動的、併發的、三色的標記清除垃圾收集器」

go 除了標準的三色收集之外,還有一個輔助回收功能,防止垃圾產生過快手機不過來的狀況。這部分代碼在 runtime.gcAssistAlloc 中。

可是 golang 並無分代收集,因此對於巨量的小對象仍是很苦手的,會致使整個 mark 過程十分長,在某些極端狀況下,甚至會致使 GC 線程佔據 50% 以上的 CPU。

所以,當程序因爲高併發等緣由形成大量小對象的gc問題時,最好可使用 sync.Pool 等對象池技術,避免大量小對象加大 GC 壓力。

 

 

go採用三色標記和寫屏障:

  • 起初全部的對象都是白色
  • 掃描找出全部可達對象,標記爲灰色,放入待處理隊列
  • 從隊列提取灰色對象,將其引用對象標記爲灰色放入隊列
  • 寫屏障監視對象的內存修改,從新標色或放回隊列

關於go的寫屏障(write barrier),能夠閱讀最近一篇比較熱的文章《Proposal: Eliminate STW stack re-scanning》。 做者主要介紹下個版本Go爲了消除STW所作的一些改進,包括寫屏障的優化方式。

併發的三色標記算法是一個經典算法,經過write barrier,維護」黑色對象不能引用白色對象」這條約束,就能夠保證程序的正確性。Go1.5會在標記階段開啓write barrier。在這個階段裏,若是用戶代碼想要執行操做,修改一個黑色對象去引用白色對象,則write barrier代碼直接將該白色對象置爲灰色。去讀源代碼實現的時候,有一個很小的細節:原版的算法中只是黑色引用白色則須要將白色標記,而Go1.5實現中是無論黑色/灰色/白色對象,只要引用了白色對象,就將這個白色對象標記。這麼作的緣由是,Go的標記位圖跟對象自己的內存是在不一樣的地方,沒法原子性地進行修改,而採用一些線程同步的實現代價又較高,因此這裏的算法作過一些變種的處理。

相關文章
相關標籤/搜索