用任何帶 GC 的語言最後都要直面 GC 問題。在之前學習 C# 的時候就被迫讀了一大堆 .NET Garbage Collection 的文檔。最近也學習了一番 golang 的垃圾回收機制,在這裏記錄一下。java
趁着這個機會我總結了一下常見的 GC 算法。分別是:引用計數法、Mark-Sweep法、三色標記法、分代收集法。c++
原理是在每一個對象內部維護一個整數值,叫作這個對象的引用計數,當對象被引用時引用計數加一,當對象不被引用時引用計數減一。當引用計數爲 0 時,自動銷燬對象。git
目前引用計數法主要用在 c++ 標準庫的 std::shared_ptr 、微軟的 COM 、Objective-C 和 PHP 中。github
可是引用計數法有個缺陷就是不能解決循環引用的問題。循環引用是指對象 A 和對象 B 互相持有對方的引用。這樣兩個對象的引用計數都不是 0 ,所以永遠不能被收集。golang
另外的缺陷是,每次對象的賦值都要將引用計數加一,增長了消耗。算法
這個算法分爲兩步,標記和清除。併發
如圖所示。jvm
可是這個算法也有一個缺陷,就是人們經常說的 STW 問題(Stop The World)。由於算法在標記時必須暫停整個程序,不然其餘線程的代碼可能會改變對象狀態,從而可能把不該該回收的對象當作垃圾收集掉。ide
當程序中的對象逐漸增多時,遞歸遍歷整個對象樹會消耗不少的時間,在大型程序中這個時間可能會是毫秒級別的。讓全部的用戶等待幾百毫秒的 GC 時間這是不能容忍的。高併發
golang 1.5之前使用的這個算法。
三色標記法是傳統 Mark-Sweep 的一個改進,它是一個併發的 GC 算法。
原理以下,
過程如上圖所示。
這個算法能夠實現 "on-the-fly",也就是在程序執行的同時進行收集,並不須要暫停整個程序。
可是也會有一個缺陷,可能程序中的垃圾產生的速度會大於垃圾收集的速度,這樣會致使程序中的垃圾愈來愈多沒法被收集掉。
使用這種算法的是 Go 1.五、Go 1.6。
分代收集也是傳統 Mark-Sweep 的一個改進。這個算法是基於一個經驗:絕大多數對象的生命週期都很短。因此按照對象的生命週期長短來進行分代。
通常 GC 都會分三代,在 java 中稱之爲新生代(Young Generation)、年老代(Tenured Generation)和永久代(Permanent Generation);在 .NET 中稱之爲第 0 代、第 1 代和第2代。
原理以下:
由於 0 代中的對象十分少,因此每次收集時遍歷都會很是快(比 1 代收集快幾個數量級)。只有內存消耗過於大的時候纔會觸發較慢的 1 代和 2 代收集。
所以,分代收集是目前比較好的垃圾回收方式。使用的語言(平臺)有 jvm、.NET 。
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的標記位圖跟對象自己的內存是在不一樣的地方,沒法原子性地進行修改,而採用一些線程同步的實現代價又較高,因此這裏的算法作過一些變種的處理。