【golang】垃圾回收機制

常見 GC 算法

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

1. 引用計數法

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

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

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

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

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

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

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

 


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

 

如圖所示。

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

當程序中的對象逐漸增多時,遞歸遍歷整個對象樹會消耗很多的時間,在大型程序中這個時間可能會是毫秒級別的。讓所有的用戶等待幾百毫秒的 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.5、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 壓力。

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