go 垃圾回收機制
用任何帶 GC 的語言最後都要直面 GC 問題。在以前學習 C# 的時候就被迫讀了一大堆 .NET Garbage Collection 的文件。最近也學習了一番 golang 的垃圾回收機制,在這裡記錄一下。
常見 GC 演算法
趁著這個機會我總結了一下常見的 GC 演算法。分別是:引用計數法、Mark-Sweep法、三色標記法、分代收集法。
1. 引用計數法
原理是在每個物件內部維護一個整數值,叫做這個物件的引用計數,當物件被引用時引用計數加一,當物件不被引用時引用計數減一。當引用計數為 0 時,自動銷燬物件。
目前引用計數法主要用在 c++ 標準庫的 std::shared_ptr 、微軟的 COM 、Objective-C 和 PHP 中。
但是引用計數法有個缺陷就是不能解決迴圈引用的問題。迴圈引用是指物件 A 和物件 B 互相持有對方的引用。這樣兩個物件的引用計數都不是 0 ,因此永遠不能被收集。
另外的缺陷是,每次物件的賦值都要將引用計數加一,增加了消耗。
2. Mark-Sweep法(標記清除法)
這個演算法分為兩步,標記和清除。
標記:從程式的根節點開始, 遞迴地 遍歷所有物件,將能遍歷到的物件打上標記。
清除:講所有未標記的的物件當作垃圾銷燬。
如圖所示:
但是這個演算法也有一個缺陷,就是人們常常說的 STW 問題(Stop The World)。因為演算法在標記時必須暫停整個程式,否則其他執行緒的程式碼可能會改變物件狀態,從而可能把不應該回收的物件當做垃圾收集掉。
當程式中的物件逐漸增多時,遞迴遍歷整個物件樹會消耗很多的時間,在大型程式中這個時間可能會是毫秒級別的。讓所有的使用者等待幾百毫秒的 GC 時間這是不能容忍的。
golang 1.5以前使用的這個演算法。
3. 三色標記法
三色標記法是傳統 Mark-Sweep 的一個改進,它是一個併發的 GC 演算法。
原理如下,
1、首先建立三個集合:白、灰、黑。
2、將所有物件放入白色集合中。
3、然後從根節點開始遍歷所有物件(注意這裡並不遞迴遍歷),把遍歷到的物件從白色集合放入灰色集合。
4、之後遍歷灰色集合,將灰色物件引用的物件從白色集合放入灰色集合,之後將此灰色物件放入黑色集合
5、重複 4 直到灰色中無任何物件
6、通過write-barrier檢測物件有變化,重複以上操作
7、收集所有白色物件(垃圾)
過程如上圖
這個演算法可以實現 “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 壓力。