之前寫了 一篇【Golang】內存管理 ,有了很多的閱讀量,那么我就接著分享一下Golang的GC相關的學習。
由于Golang的GC機制一直在持續迭代,本文敘述的主要是Go1.9版本及以后的GC機制,該版本中Golang引入了 混合寫屏障大幅度地優化了STW的時間。具體GC機制的版本迭代可以參考 :GC的過去、現在和未來
基本概念
GC的對象
不再被需要的內存塊,如果無法回收這部分內存將無法重復使用
內存泄漏
不再被需要的內存塊,未在預期時間以內被回收的稱為內存泄漏
常見的GC方法:
1.【PHP,Swift,Python等】引用計數法:
- 每個對象維護一個自身被引用的次數,當一個引用者消息后該計數-1;
- 它的優勢為GC時查找需要回收的對象很簡單:只需要找到引用計數為0的對象即可;
- 它的劣勢為在寫入引用關系時需要額外維護引用計數,帶來一定開銷 ;
2.【Golang】標記清除法: - 每次GC時通過從根對象出發遞歸地查詢所有對象的引用關系,將無引用的對象進行標記后再行清理。
- 它的優勢在于寫入引用關系時無額外開銷
- 它的劣勢在于GC時查詢引用關系有一定開銷 ,有時還需要STW從而影響性能
STW: Stop the world
為了避免在GC的過程中,對象之間的引用關系發生新的變更使得GC的結果發生錯誤(比如GC過程中新增了一個引用,但是由于未掃描到該引用導致將被引用的對象清除了),停止所有正在運行的協程。
STW對性能有一些影響,Golang目前已經可以做到1ms以下的STW。
三色標記法
Golang中的GC方式為標記后清除,所采用的邏輯為——三色標記法:一開始全部的對象都是白色的

- 從root對象開始(全局變量+全局棧+當前活躍的goroutines中的棧),將它們加入到灰色隊列中,進行遍歷:類似于二叉樹使用隊列進行BFS遍歷的過程
- 從灰色隊列中pop出一個對象
- 將其引用的對象都置為灰色,加入到灰色隊列中
- 將該對象置為黑色
- 直到灰色隊列為空時,剩余的無引用的白色對象即會被清除
內存回收
看不太明白的可以參考我的上一篇文章:【Golang】內存管理
- 被回收的內存塊會被回收到MCache(Processor私有內存)結構中 ,對應的MSpan(塊類型)的allocBits的bitmap中對應的位置會被從1改為 0
- MSpan中除了allocBits還有另一個bitmap:GCMarkBits用于存放需要清除的標記:黑色=1,白色=0,一次GC之后所有MSpan中的GCMarkBits和AllocBits應該完全相同,因為只有黑色的內存對象在GC之后才會保留下來
寫屏障
上文有提到為了避免GC的過程中新修改的引用關系到GC的結果發生錯誤,我們需要進行STW。但是STW會影響程序的性能,所以我們要通過寫屏障技術盡可能地縮短STW的時間。
造成引用對象丟失的條件
- 一個黑色的節點A新增了指向白色節點C的引用
- 并且白色節點C沒有除了A之外的其他灰色節點的引用,或者存在但是在GC過程中被刪除了
以上兩個條件需要同時滿足:滿足條件1時說明節點A已掃描完畢,A指向C的引用無法再被掃描到;滿足條件2時說明白色節點C無其他灰色節點的引用了,即掃描結束后會被忽略 。
寫屏障破壞兩個條件其一即可
- 破壞條件1 => Dijistra寫屏障
- 滿足強三色不變性:黑色節點不允許引用白色節點
- 當黑色節點新增了白色節點的引用時,將對應的白色節點改為灰色
- 破壞條件2 => Yuasa寫屏障
- 滿足弱三色不變性:黑色節點允許引用白色節點,但是該白色節點有其他灰色節點間接的引用(確保不會被遺漏)
- 當白色節點被刪除了一個引用時,悲觀地認為它一定會被一個黑色節點新增引用,所以將它置為灰色
寫屏障的分類
注意:由于棧上的操作需要保證性能,所以所有的寫屏障均只針對堆上的對象。

GC的時機
- 每次內存分配時檢查當前內存分配量是否已達到閾值(環境變量GOGC):默認100%,即當內存擴大一倍時啟用GC
- 定時觸發:當最近2分鐘未觸發過GC時,會觸發一次GC
- 通過runtime.GC()手動觸發
GC的優化
分配的對象越多,GC性能就越差,所以需要減少對象分配的個數,比如對象復用,使用sync.Pool
注意:sync.Pool類似于緩存,其中的對象會被定期清理(GC時清理),不能放置像是數據庫連接這樣需要穩定存儲的數據
參考
1.三色標記法是什么?
2.Go語言垃圾收集器原理

喜歡的朋友記得點贊、收藏、關注哦!!!