轉自http://legendtkl.com/2017/04/28/golang-gc/git
另外還有一篇這個https://studygolang.com/articles/14497github
Golang 從第一個版本以來,GC 一直是你們詬病最多的。可是每個版本的發佈基本都伴隨着 GC 的改進。下面列出一些比較重要的改動。golang
這一小節介紹三種經典的 GC 算法:引用計數(reference counting)、標記-清掃(mark & sweep)、節點複製(Copying Garbage Collection),分代收集(Generational Garbage Collection)。算法
引用計數的思想很是簡單:每一個單元維護一個域,保存其它單元指向它的引用數量(相似有向圖的入度)。當引用數量爲 0 時,將其回收。引用計數是漸進式的,可以將內存管理的開銷分佈到整個程序之中。C++ 的 share_ptr 使用的就是引用計算方法。數據結構
引用計數算法實現通常是把全部的單元放在一個單元池裏,好比相似 free list。這樣全部的單元就被串起來了,就能夠進行引用計數了。新分配的單元計數值被設置爲 1(注意不是 0,由於申請通常都說 ptr = new object 這種)。每次有一個指針被設爲指向該單元時,該單元的計數值加 1;而每次刪除某個指向它的指針時,它的計數值減 1。當其引用計數爲 0 的時候,該單元會被進行回收。雖然這裏說的比較簡單,實現的時候仍是有不少細節須要考慮,好比刪除某個單元的時候,那麼它指向的全部單元都須要對引用計數減 1。那麼若是這個時候,發現其中某個指向的單元的引用計數又爲 0,那麼是遞歸的進行仍是採用其餘的策略呢?遞歸處理的話會致使系統顛簸。關於這些細節這裏就不討論了,能夠參考文章後面的給的參考資料。併發
標記-清掃算法是第一種自動內存管理,基於追蹤的垃圾收集算法。算法思想在 70 年代就提出了,是一種很是古老的算法。內存單元並不會在變成垃圾馬上回收,而是保持不可達狀態,直到到達某個閾值或者固定時間長度。這個時候系統會掛起用戶程序,也就是 STW,轉而執行垃圾回收程序。垃圾回收程序對全部的存活單元進行一次全局遍歷肯定哪些單元能夠回收。算法分兩個部分:標記(mark)和清掃(sweep)。標記階段代表全部的存活單元,清掃階段將垃圾單元回收。可視化能夠參考下圖。app
標記-清掃算法的優勢也就是基於追蹤的垃圾回收算法具備的優勢:避免了引用計數算法的缺點(不能處理循環引用,須要維護指針)。缺點也很明顯,須要 STW。函數
三色標記算法是對標記階段的改進,原理以下:oop
三色標記的一個明顯好處是可以讓用戶程序和 mark 併發的進行,具體能夠參考論文:《On-the-fly garbage collection: an exercise in cooperation.》。Golang 的 GC 實現也是基於這篇論文,後面再具體說明。
節點複製也是基於追蹤的算法。其將整個堆等分爲兩個半區(semi-space),一個包含現有數據,另外一個包含已被廢棄的數據。節點複製式垃圾收集從切換(flip)兩個半區的角色開始,而後收集器在老的半區,也就是 Fromspace 中遍歷存活的數據結構,在第一次訪問某個單元時把它複製到新半區,也就是 Tospace 中去。在 Fromspace 中全部存活單元都被訪問過以後,收集器在 Tospace 中創建一個存活數據結構的副本,用戶程序能夠從新開始運行了。
基於追蹤的垃圾回收算法(標記-清掃、節點複製)一個主要問題是在生命週期較長的對象上浪費時間(長生命週期的對象是不須要頻繁掃描的)。同時,內存分配存在這麼一個事實 「most object die young」。基於這兩點,分代垃圾回收算法將對象按生命週期長短存放到堆上的兩個(或者更多)區域,這些區域就是分代(generation)。對於新生代的區域的垃圾回收頻率要明顯高於老年代區域。
分配對象的時候重新生代裏面分配,若是後面發現對象的生命週期較長,則將其移到老年代,這個過程叫作 promote。隨着不斷 promote,最後新生代的大小在整個堆的佔用比例不會特別大。收集的時候集中主要精力在新生代就會相對來講效率更高,STW 時間也會更短。
在說 Golang 的具體垃圾回收流程時,咱們先來看一下幾個基本的問題。
在堆上分配大於 32K byte 對象的時候進行檢測此時是否知足垃圾回收條件,若是知足則進行垃圾回收。
1 |
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { |
上面是自動垃圾回收,還有一種是主動垃圾回收,經過調用 runtime.GC(),這是阻塞式的。
1 |
// GC runs a garbage collection and blocks the caller until the |
觸發條件主要關注下面代碼中的中間部分:forceTrigger || memstats.heap_live >= memstats.gc_trigger
。forceTrigger 是 forceGC 的標誌;後面半句的意思是當前堆上的活躍對象大於咱們初始化時候設置的 GC 觸發閾值。在 malloc 以及 free 的時候 heap_live 會一直進行更新,這裏就再也不展開了。
1 |
// gcShouldStart returns true if the exit condition for the _GCoff |
三色標記法,主要流程以下:
關於上圖有幾點須要說明的是。
另外針對上圖各個階段對應 GCPhase 以下:
關於 write barrier,徹底能夠另外寫成一篇文章,因此這裏只簡單介紹一下,這篇文章的重點仍是 Golang 的 GC。垃圾回收中的 write barrier 能夠理解爲編譯器在寫操做時特地插入的一段代碼,對應的還有 read barrier。
爲何須要 write barrier,很簡單,對於和用戶程序併發運行的垃圾回收算法,用戶程序會一直修改內存,因此須要記錄下來。
Golang 1.7 以前的 write barrier 使用的經典的 Dijkstra-style insertion write barrier [Dijkstra ‘78], STW 的主要耗時就在 stack re-scan 的過程。自 1.8 以後採用一種混合的 write barrier 方式 (Yuasa-style deletion write barrier [Yuasa ‘90] 和 Dijkstra-style insertion write barrier [Dijkstra ‘78])來避免 re-scan。具體的能夠參考 17503-eliminate-rescan。
下面的源碼仍是基於 go1.8rc3。這個版本的 GC 代碼相比以前改動仍是挺大的,咱們下面儘可能只關注主流程。垃圾回收的代碼主要集中在函數 gcStart()
中。
1 |
// gcStart 是 GC 的入口函數,根據 gcMode 作處理。 |
在 GC 開始以前的準備工做。
1 |
func gcStart(mode gcMode, forceTrigger bool) { |
Mark 階段是並行的運行,經過在後臺一直運行 mark worker 來實現。
1 |
func gcStart(mode gcMode, forceTrigger bool) { |
Mark 階段的標記代碼主要在函數 gcDrain() 中實現。
1 |
// gcDrain scans roots and objects in work buffers, blackening grey |
mark termination 階段會 stop the world。函數實如今 gcMarkTermination()
。1.8 版本已經不會再對 goroutine stack 進行 re-scan 了。細節有點多,這裏不細說了。
1 |
func gcMarkTermination() { |
清掃相對來講就簡單不少了。
1 |
func gcSweep(mode gcMode) { |
對於並行式清掃,在 GC 初始化的時候就會啓動 bgsweep()
,而後在後臺一直循環。
1 |
func bgsweep(c chan int) { |
不論是阻塞式仍是並行式,都是經過 sweepone()
函數來作清掃工做的。若是對於上篇文章 Golang 內存管理 熟悉的話,這個地方就很好理解。內存管理都是基於 span 的,mheap_ 是一個全局的變量,全部分配的對象都會記錄在 mheap_ 中。在標記的時候,咱們只要找到對對象對應的 span 進行標記,清掃的時候掃描 span,沒有標記的 span 就能夠回收了。
1 |
// sweeps one span |
這裏介紹一下任務隊列,或者說灰色對象管理。每一個 P 上都有一個 gcw 用來管理灰色對象(get 和 put),gcw 的結構就是 gcWork。gcWork 中的核心是 wbuf1 和 wbuf2,裏面存儲就是灰色對象,或者說是 work(下面就所有統一叫作 work)。
1 |
type p struct { |
既然每一個 P 上有一個 work buffer,那麼是否是還有一個全局的 work list 呢?是的。經過在每一個 P 上綁定一個 work buffer 的好處和 cache 同樣,不須要加鎖。
1 |
var work struct { |
那麼爲何使用兩個 work buffer (wbuf1 和 wbuf2)呢?我下面舉個例子。好比我如今要 get 一個 work 出來,先從 wbuf1 中取,wbuf1 爲空的話則與 wbuf2 swap 再 get。在其餘時間將 work buffer 中的 full 或者 empty buffer 移到 global 的 work 中。這樣的好處在於,在 get 的時候去全局的 work 裏面取(多個 goroutine 去取會有競爭)。這裏有趣的是 global 的 work list 是 lock-free 的,經過原子操做 cas 等實現。下面列舉幾個函數看一下 gcWrok。
初始化。
1 |
func (w *gcWork) init() { |
put。
1 |
// put enqueues a pointer for the garbage collector to trace. |
get。
1 |
// get dequeues a pointer for the garbage collector to trace, blocking |
咱們上面講了兩種 GC 觸發方式:自動檢測和用戶主動調用。除此以後 Golang 自己還會對運行狀態進行監控,若是超過兩分鐘沒有 GC,則觸發 GC。監控函數是 sysmon()
,在主 goroutine 中啓動。
1 |
// The main goroutine |