golang GC 垃圾回收機制
垃圾回收(Garbage Collection,簡稱GC)是程式語言中提供的自動的記憶體管理機制,自動釋放不需要的物件,讓出儲存器資源,無需程式設計師手動執行。
Golang中的垃圾回收主要應用三色標記法,GC過程和其他使用者goroutine可併發執行,但需要一定時間的STW(stop the world),STW的過程中,CPU不執行使用者程式碼,全部用於垃圾回收,這個過程的影響很大,Golang進行了多次的迭代優化來解決這個問題。
Go V1.3之前的標記-清除(mark and sweep)演算法
此演算法主要有兩個主要的步驟:
- 標記(Mark phase)
- 清除(Sweep phase)
第一步,暫停程式業務邏輯, 找出不可達的物件,然後做上標記。第二步,回收標記好的物件。
操作非常簡單,但是有一點需要額外注意:mark and sweep演算法在執行的時候,需要程式暫停!即 STW(stop the world)
。也就是說,這段時間程式會卡在哪兒。
第二步, 開始標記,程式找出它所有可達的物件,並做上標記。如下圖所示:
第三步, 標記完了之後,然後開始清除未標記的物件. 結果如下.
第四步, 停止暫停,讓程式繼續跑。然後迴圈重複這個過程,直到process程式生命週期結束。
標記-清掃(mark and sweep)的缺點
- STW,stop the world;讓程式暫停,程式出現卡頓 (重要問題)
- 標記需要掃描整個heap
- 清除資料會產生heap碎片
所以Go V1.3版本之前就是以上來實施的, 流程是
Go V1.3 做了簡單的優化,將STW提前, 減少STW暫停的時間範圍.如下所示
這裡面最重要的問題就是:mark-and-sweep 演算法會暫停整個程式 。
Go是如何面對並這個問題的呢?接下來G V1.5版本 就用三色併發標記法來優化這個問題.
Go V1.5的三色併發標記法
三色標記法 實際上就是通過三個階段的標記來確定清楚的物件都有哪些. 我們來看一下具體的過程.
第一步 , 就是隻要是新建立的物件,預設的顏色都是標記為“白色”.
這裡面需要注意的是, 所謂“程式”, 則是一些物件的跟節點集合.
所以上圖,可以轉換如下的方式來表示.
第二步, 每次GC回收開始, 然後從根節點開始遍歷所有物件,把遍歷到的物件從白色集合放入“灰色”集合。
第三步, 遍歷灰色集合,將灰色物件引用的物件從白色集合放入灰色集合,之後將此灰色物件放入黑色集合
第四步, 重複第三步, 直到灰色中無任何物件.
第五步: 回收所有的白色標記表的物件. 也就是回收垃圾.
以上便是三色併發標記法
, 不難看出,我們上面已經清楚的體現三色
的特性, 那麼又是如何實現並行的呢?
Go是如何解決標記-清除(mark and sweep)演算法中的卡頓(stw,stop the world)問題的呢?
沒有STW的三色標記法
我們還是基於上述的三色併發標記法來說, 他是一定要依賴STW的. 因為如果不暫停程式, 程式的邏輯改變物件引用關係, 這種動作如果在標記階段做了修改,會影響標記結果的正確性。我們舉一個場景.
如果三色標記法, 標記過程不使用STW將會發生什麼事情?
可以看出,有兩個問題, 在三色標記法中,是不希望被髮生的
- 條件1: 一個白色物件被黑色物件引用(白色被掛在黑色下)
- 條件2: 灰色物件與它之間的可達關係的白色物件遭到破壞(灰色同時丟了該白色)
當以上兩個條件同時滿足時, 就會出現物件丟失現象!
當然, 如果上述中的白色物件3, 如果他還有很多下游物件的話, 也會一併都清理掉.
為了防止這種現象的發生,最簡單的方式就是STW,直接禁止掉其他使用者程式對物件引用關係的干擾,但是STW的過程有明顯的資源浪費,對所有的使用者程式都有很大影響,如何能在保證物件不丟失的情況下合理的儘可能的提高GC效率,減少STW時間呢?
答案就是, 那麼我們只要使用一個機制,來破壞上面的兩個條件就可以了.
屏障機制
我們讓GC回收器,滿足下面兩種情況之一時,可保物件不丟失. 所以引出兩種方式.
“強-弱” 三色不變式
- 強三色不變式
不存在黑色物件引用到白色物件的指標。
- 弱三色不變式
所有被黑色物件引用的白色物件都處於灰色保護狀態.
為了遵循上述的兩個方式,Golang團隊初步得到了如下具體的兩種屏障方式“插入屏障”, “刪除屏障”.
插入屏障
具體操作
: 在A物件引用B物件的時候,B物件被標記為灰色。(將B掛在A下游,B必須被標記為灰色)
滿足
: 強三色不變式. (不存在黑色物件引用白色物件的情況了, 因為白色會強制變成灰色)
偽碼如下:
新增下游物件(當前下游物件slot, 新下游物件ptr) {
//1
標記灰色(新下游物件ptr)
//2
當前下游物件slot = 新下游物件ptr
}
場景:
A.新增下游物件(nil, B) //A 之前沒有下游, 新新增一個下游物件B, B被標記為灰色
A.新增下游物件(C, B) //A 將下游物件C 更換為B, B被標記為灰色
這段偽碼邏輯就是寫屏障,. 我們知道,黑色物件的記憶體槽有兩種位置, 棧
和堆
. 棧空間的特點是容量小,但是要求相應速度快,因為函式呼叫彈出頻繁使用, 所以“插入屏障”機制,在棧空間的物件操作中不使用. 而僅僅使用在堆空間物件的操作中.
接下來,我們用幾張圖,來模擬整個一個詳細的過程, 希望您能夠更可觀的看清晰整體流程。
但是如果棧不新增,當全部三色標記掃描之後,棧上有可能依然存在白色物件被引用的情況(如上圖的物件9). 所以要對棧重新進行三色標記掃描, 但這次為了物件不丟失, 要對本次標記掃描啟動STW暫停. 直到棧空間的三色標記結束.
最後將棧和堆空間 掃描剩餘的全部 白色節點清除. 這次STW大約的時間在10~100ms間.
刪除屏障
具體操作
: 被刪除的物件,如果自身為灰色或者白色,那麼被標記為灰色。
滿足
: 弱三色不變式. (保護灰色物件到白色物件的路徑不會斷)
虛擬碼:
新增下游物件(當前下游物件slot, 新下游物件ptr) {
//1
if (當前下游物件slot是灰色 || 當前下游物件slot是白色) {
標記灰色(當前下游物件slot) //slot為被刪除物件, 標記為灰色
}
//2
當前下游物件slot = 新下游物件ptr
}
場景:
A.新增下游物件(B, nil) //A物件,刪除B物件的引用。 B被A刪除,被標記為灰(如果B之前為白)
A.新增下游物件(B, C) //A物件,更換下游B變成C。 B被A刪除,被標記為灰(如果B之前為白)
接下來,我們用幾張圖,來模擬整個一個詳細的過程, 希望您能夠更可觀的看清晰整體流程。
這種方式的回收精度低,一個物件即使被刪除了最後一個指向它的指標也依舊可以活過這一輪,在下一輪GC中被清理掉。
Go V1.8的混合寫屏障(hybrid write barrier)機制
插入寫屏障和刪除寫屏障的短板:
- 插入寫屏障:結束時需要STW來重新掃描棧,標記棧上引用的白色物件的存活;
- 刪除寫屏障:回收精度低,GC開始時STW掃描堆疊來記錄初始快照,這個過程會保護開始時刻的所有存活物件。
Go V1.8版本引入了混合寫屏障機制(hybrid write barrier),避免了對棧re-scan的過程,極大的減少了STW的時間。結合了兩者的優點。
混合寫屏障規則
具體操作
:
1、GC開始將棧上的物件全部掃描並標記為黑色(之後不再進行第二次重複掃描,無需STW),
2、GC期間,任何在棧上建立的新物件,均為黑色。
3、被刪除的物件標記為灰色。
4、被新增的物件標記為灰色。
滿足
: 變形的弱三色不變式.
虛擬碼:
新增下游物件(當前下游物件slot, 新下游物件ptr) {
//1
標記灰色(當前下游物件slot) //只要當前下游物件被移走,就標記灰色
//2
標記灰色(新下游物件ptr)
//3
當前下游物件slot = 新下游物件ptr
}
這裡我們注意, 屏障技術是不在棧上應用的,因為要保證棧的執行效率。
混合寫屏障的具體場景分析
接下來,我們用幾張圖,來模擬整個一個詳細的過程, 希望您能夠更可觀的看清晰整體流程。
注意混合寫屏障是Gc的一種屏障機制,所以只是當程式執行GC的時候,才會觸發這種機制。
GC開始:掃描棧區,將可達物件全部標記為黑
場景一: 物件被一個堆物件刪除引用,成為棧物件的下游
虛擬碼
//前提:堆物件4->物件7 = 物件7; //物件7 被 物件4引用
棧物件1->物件7 = 堆物件7; //將堆物件7 掛在 棧物件1 下游
堆物件4->物件7 = null; //物件4 刪除引用 物件7
場景二: 物件被一個棧物件刪除引用,成為另一個棧物件的下游
虛擬碼
new 棧物件9;
物件8->物件3 = 物件3; //將棧物件3 掛在 棧物件9 下游
物件2->物件3 = null; //物件2 刪除引用 物件3
場景三:物件被一個堆物件刪除引用,成為另一個堆物件的下游
虛擬碼
堆物件10->物件7 = 堆物件7; //將堆物件7 掛在 堆物件10 下游
堆物件4->物件7 = null; //物件4 刪除引用 物件7
場景四:物件從一個棧物件刪除引用,成為另一個堆物件的下游
虛擬碼
堆物件10->物件7 = 堆物件7; //將堆物件7 掛在 堆物件10 下游
堆物件4->物件7 = null; //物件4 刪除引用 物件7
Golang中的混合寫屏障滿足弱三色不變式
,結合了刪除寫屏障和插入寫屏障的優點,只需要在開始時併發掃描各個goroutine的棧,使其變黑並一直保持,這個過程不需要STW,而標記結束後,因為棧在掃描後始終是黑色的,也無需再進行re-scan操作了,減少了STW的時間。
總結
以上便是Golang的GC全部的標記-清除邏輯及場景演示全過程。
GoV1.3- 普通標記清除法,整體過程需要啟動STW,效率極低。
GoV1.5- 三色標記法, 堆空間啟動寫屏障,棧空間不啟動,全部掃描之後,需要重新掃描一次棧(需要STW),效率普通
GoV1.8-三色標記法,混合寫屏障機制, 棧空間不啟動,堆空間啟動。整個過程幾乎不需要STW,效率較高。