如果您覺得有幫助,麻煩點個贊或者關注支持一下,定期推送文章
Golang 三色標記、混合寫屏障GC模式圖文全分析
垃圾回收(Garbage Collection,簡稱GC)是編程語言中提供的自動的內存管理機制,自動釋放不需要的對象,讓出存儲器資源,無需程序員手動執行。
Golang中的垃圾回收主要應用三色標記法,GC過程和其他用戶goroutine可併發運行,但需要一定時間的STW(stop the world),STW的過程中,CPU不執行用戶代碼,全部用於垃圾回收,這個過程的影響很大,Golang進行了多次的迭代優化來解決這個問題,本文將逐步推進Golang中GC的每次推進,來理解Gc的原理。
零、內容提綱
本文將系統的詳細介紹Golang中GC的全分析過程,包括垃圾回收的方式遞進。
內容概要:
- G0 V1.3之前的標記-清除(mark and sweep)算法
- Go V1.3之前的標記-清除(mark and sweep)的缺點
- Go V1.5的三色併發標記法
- Go V1.5的三色標記爲什麼需要STW
- Go V1.5的三色標記爲什麼需要屏障機制(“強-弱” 三色不變式、插入屏障、刪除屏障 )
- Go V1.8混合寫屏障機制
- Go V1.8混合寫屏障機制的全場景分析
一、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的時間。結合了兩者的優點。
混合寫屏障規則
具體操作:
- GC開始將棧上的對象全部掃描並標記爲黑色(之後不再進行第二次重複掃描,無需STW),
- GC期間,任何在棧上創建的新對象,均爲黑色。
- 被刪除的對象標記爲灰色。
- 被添加的對象標記爲灰色。
滿足:形的弱三色不變式.
僞代碼:
添加下游對象(當前下游對象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;
對象9->對象3 = 對象3; //將棧對象3 掛在 棧對象9 下游
對象2->對象3 = null; //對象2 刪除引用 對象3
場景三:對象被一個堆對象刪除引用,成爲另一個堆對象的下游
場景四:對象從一個棧對象刪除引用,成爲另一個堆對象的下游
棧對象1->對象2 = null; //對象1 刪除引用 對象2
堆對象4->對象2 = 棧對象2; //對象4 添加 下游 棧對象2
堆對象4->對象7 = null; //對象4 刪除引用 對象7nu
Golang中的混合寫屏障滿足弱三色不變式,結合了刪除寫屏障和插入寫屏障的優點,只需要在開始時併發掃描各個goroutine的棧,使其變黑並一直保持,這個過程不需要STW,而標記結束後,因爲棧在掃描後始終是黑色的,也無需再進行re-scan操作了,減少了STW的時間。
七、總結
以上便是Golang的GC全部的標記-清除邏輯及場景演示全過程。
- GoV1.3- 普通標記清除法,整體過程需要啓動STW,效率極低。
- GoV1.5- 三色標記法, 堆空間啓動寫屏障,棧空間不啓動,全部掃描之後,需要重新掃描一次棧(需要STW),效率普通
- GoV1.8-三色標記法,混合寫屏障機制, 棧空間不啓動,堆空間啓動。整個過程幾乎不需要STW,效率較高。
如果您覺得有幫助,麻煩點個贊或者關注支持一下,定期推送文章