一绊起、GC觸發(fā)
-
內存分配量達到閥值觸發(fā) GC
每次內存分配時蚊伞,都會檢查當前內存分配量是否已達到閥值泼舱,如果達到閥值則立即啟動 GC:- 閥值 = 上次 GC 內存分配量 * 內存增長率
- 內存增長率由環(huán)境變量 GOGC 控制躁绸,默認為 100裕循,即每當內存擴大一倍時啟動 GC
-
定期觸發(fā) GC
默認情況下,最長 2 分鐘净刮,由sysmon觸發(fā)一次 GC费韭,這個間隔在 src/runtime/proc.go:forcegcperiod 變量中被聲明 -
手動觸發(fā)
程序代碼中也可以使用 runtime.GC()來手動觸發(fā) GC。這主要用于 GC 性能測試和統(tǒng)計庭瑰。
二星持、 v1.3 標記-清除算法
1、 v1.3 之前
整體流程圖
具體步驟
-
啟動STW(Stop The World)弹灭,暫停程序業(yè)務邏輯
-
從根對象開始標記督暂,找出所有可達的對象,并做上標記穷吮。如下圖所示:
-
標記完成后逻翁,清除未標記的對象
- 停止STW,讓程序繼續(xù)運行捡鱼。然后循環(huán)重復這個過程八回,直到process程序生命周期結束。
缺點:
- STW讓程序暫停驾诈,CPU全部用于垃圾回收缠诅,程序出現(xiàn)卡頓(重要問題)
- 標記需要掃描整個heap
- 清楚數(shù)據(jù)會產(chǎn)生heap碎片
2、 v1.3優(yōu)化Mark & Sweep
由于未被標記的不可達對象乍迄,基本不會再次被引用(由于程序中沒有對象擁有不可達對象的地址管引,所以難以再被其他對象引用)。因此闯两,在Mark標記完成后褥伴,就停止STW谅将,讓程序恢復運行,可以減少STW的時間重慢,同時也不會影響Sweep清除的正確性饥臂。
所以,go在v1.3版本做了簡單的優(yōu)化似踱,將STW的步驟提前, 減少STW暫停的時間范圍擅笔,同時并發(fā)執(zhí)行Sweep清除。如下所示:
優(yōu)化后的GC仍存在STW的問題屯援,Go V1.5版本使用了三色并發(fā)標記法來繼續(xù)優(yōu)化這個問題。
三念脯、v1.5 三色并發(fā)標記狞洋、插入寫屏障、刪除寫屏障
三色標記的一個明顯好處是能夠讓用戶程序和 mark 并發(fā)的進行
“三色”只是為了敘述上方便抽象出來的一種說法绿店,實際上對象并沒有顏色之分吉懊。這里的“三色”,對應了垃圾回收過程中對象的三種狀態(tài):
- 黑色:已被回收器訪問到的對象假勿,其子對象也已被被回收器訪問到
- 灰色:已被回收器訪問到的對象借嗽,但其可能仍存在子對象未被回收器訪問到。
- 白色:未被回收器訪問到的對象(潛在的垃圾)转培,其內存可能會被垃圾收集器回收恶导。
1、三色并發(fā)標記--具體步驟:
-
初始將所有內存標記為白色浸须,將所有對象放入白色集合中
-
然后將 roots 加入worklist(進入worklist即被視為變成灰色)
-
從根節(jié)點開始遍歷所有對象惨寿,把遍歷到的對象從白色集合放入“灰色”集合。
(本次遍歷只會對根節(jié)點下的子節(jié)點進行1次遍歷删窒,是非遞歸遍歷裂垦,僅遍歷1層)
-
遍歷灰色集合,將灰色對象引用的對象從白色集合放入灰色集合肌索,之后將此灰色對象放入黑色集合蕉拢。
(這一次遍歷只掃描灰色對象,將灰色對象的第一層遍歷可抵達的對象由白色變?yōu)榛疑?將灰色對象標記為黑色诚亚,并將其從灰色標記表移動到黑色標記表)
-
重復第3步, 直到灰色標記表中無任何對象
-
回收所有的白色標記表的對象.
但是這里面可能會有很多并發(fā)流程均會被掃描晕换,執(zhí)行并發(fā)流程的內存可能相互依賴,為了在GC過程中保證數(shù)據(jù)的安全站宗,我們在開始三色標記之前就會加上STW届巩,在掃描確定黑白對象之后再放開STW。但是很明顯這樣的GC掃描的性能實在是太低了份乒。
2恕汇、沒有STW腕唧,帶來漏標問題
假設我們執(zhí)行三色并發(fā)標記時,不執(zhí)行STW瘾英。用戶程序枣接,有可能將1個灰色對象G下白色子對象W的引用,轉移給1個黑色對象B缺谴。在GC時會誤刪除對象W但惶,從而導致程序異常。詳見下圖:
由上圖可以看出湿蛔,有兩種情況膀曾,在三色標記法中,是不希望被發(fā)生的:
- 條件1: 一個白色對象被黑色對象引用(白色被掛在黑色下)
- 條件2: 灰色對象與它之間的可達關系的白色對象遭到破壞(灰色同時丟了該白色)
如果當以上兩個條件同時滿足時阳啥,就會出現(xiàn)對象丟失現(xiàn)象添谊!為了防止這種現(xiàn)象的發(fā)生,我們只要使用一種機制察迟,嘗試去破壞上面的兩個必要條件就可以了斩狱。這樣也可以避免STW帶來的資源浪費問題。
3扎瓶、屏障機制
破壞上面的兩個必要條件所踊,有兩種方式:
-
強三色不變式:不允許黑色對象引用任何白色對象
-
弱三色不變式:允許黑色對象引用白色對象,但該白色對象的可達路徑中必須存在灰色對象
在GC源碼中對應兩種屏障機制:“Dijkstra 插入寫屏障”概荷、“Yuasa 刪除寫屏障”秕岛。
-
Dijkstra 插入寫屏障
具體操作
:在A對象引用C對象的時候,C對象被標記為灰色误证。(將C掛在A下游瓣蛀,C必須被標記為灰色)
滿足
:強三色不變式. (不存在黑色對象引用白色對象的情況了, 因為白色會強制變成灰色)
偽碼如下
:
// 添加下游對象
writePointer(slot, ptr):
// 標記灰色(新下游對象ptr)
shade(ptr)
// 當前下游對象slot = 新下游對象ptr
*slot=ptr
-
Yuasa 刪除寫屏障
具體操作
:從對象B被刪除的對象C雷厂,如果對象C自身為灰色或者白色惋增,那么對象C被標記為灰色。
滿足
:弱三色不變式. (保護灰色對象到白色對象的路徑不會斷)
偽碼如下
:
// 添加下游對象
writePointer(slot, ptr):
// 如果當前對象是灰色或白色
if ( isGrey(slot) || isWhite(slot) )
// 標記灰色(當前下游對象ptr)
shade(*slot)
// 當前下游對象slot = 新下游對象ptr
*slot = ptr
-
插入寫屏障與刪除寫屏障的缺點:
- 插入寫屏障:結束時需要STW來重新掃描棧改鲫,標記棧上引用的白色對象的存活诈皿。僅適用于堆(程序運行基本在棧中,存在大量變量聲明像棘、賦值及函數(shù)調用稽亏,若棧中使用插入寫屏障,將極大增加復雜度缕题、降低性能)
- 刪除寫屏障:回收精度低截歉,GC開始時STW掃描堆棧來記錄初始快照,這個過程會保護開始時刻的所有存活對象烟零。僅適用于堆
-
整體流程
1. 初始化GC
任務瘪松,包括開啟寫屏障(write barrier
)和開啟輔助GC(mutator assist)
咸作,統(tǒng)計root
對象的任務數(shù)量等,這個過程需要STW
宵睦。
2. 掃描所有 root 對象(全局指針记罚、goroutine(G)
棧上的指針(掃描對應G
棧時需停止該G
)),將其加入灰色隊列壳嚎,并循環(huán)處理灰色隊列的對象桐智,直到灰色隊列為空,該過程后臺并行執(zhí)行
3. 完成標記工作烟馅,重新掃描(re-scan
)全局指針和棧说庭。因為 Mark是并行執(zhí)行的,且棧中不適用插入寫屏障郑趁,所以棧中可能會存在新的未掃描的對象刊驴。同時這個re-scan
過程會執(zhí)行STW
。
4. 按照標記結果回收所有的白色對象穿撮,該過程后臺并行執(zhí)行。
四痪欲、v1.8 混合寫屏障
Go V1.8版本引入了混合寫屏障機制(hybrid write barrier)悦穿,避免了對棧re-scan的過程,極大的減少了STW的時間业踢。結合了兩者的優(yōu)點栗柒。
整體流程
:
1. GC開始將棧上的可達對象全部掃描并標記為黑色(當前過程無需STW)
2. GC開始執(zhí)行標記操作,任何在堆\棧上創(chuàng)建的新對象知举,均為黑色瞬沦。
3. 標記結束,開始STW雇锡,重新掃描全局指針逛钻,不再rescan棧,并執(zhí)行其他相關操作
4. 關閉STW回收未標記對象锰提,調整下一次GC pacing
滿足
:變形的弱三色不變式曙痘。
偽碼如下
:
// 添加下游對象
writePointer(slot, ptr):
// 標記灰色(當前下游對象ptr)
shade(*slot)
// 如果當前堆棧對象是黑色
if current stack is grey:
// 標記灰色(新下游對象ptr)
shade(ptr)
// 當前下游對象slot = 新下游對象ptr
*slot = ptr
通過以下場景,我們看下在v1.5和v1.8中的GC變化
v1.5 | v1.8 | |
---|---|---|
堆對象A --x--> 堆對象B立肘,棧對象C ----> 堆對象B | 堆中刪除寫屏障標記B為灰色 | 堆中刪除寫屏障標記B為灰色 |
堆對象A ----> 新堆對象B | 堆中插入寫屏障標記B為灰色 | 堆中創(chuàng)建的新對象默認均為黑色 |
棧對象A --x--> 棧對象B边坤,棧對象C ----> 棧對象B | rescan后,最終標記B為黑色 | GC開始時谅年,已將B標記為黑色 |
棧對象A ----> 新棧對象B | rescan后茧痒,最終標記B為黑色 | 棧中創(chuàng)建的新對象默認均為黑色 |
棧對象A ----> 新堆對象B | rescan后,最終標記B為黑色 | 堆中創(chuàng)建的新對象默認均為黑色 |
棧對象A --x--> 棧對象B | rescan后融蹂,最終被清除 | GC開始時旺订,已將B標記為黑色弄企;等待下次GC清除 |
五、3個版本Mark&Sweep對比
- GoV1.3 - 普通標記清除法耸峭,整體過程需要啟動STW桩蓉,效率極低。
- GoV1.5 - 三色標記法劳闹, 堆空間啟動寫屏障院究,棧空間不啟動本涕,全部掃描之后业汰,需要重新掃描一次棧(需要STW),效率普通
- GoV1.8 - 三色標記法菩颖,混合寫屏障機制样漆, 棧空間不啟動晦闰,堆空間啟動放祟。整個過程幾乎不需要STW,效率較高呻右。
六跪妥、GC Sweep
Go 提供2種方式來清理內存:
- 在后臺啟動一個 worker 等待清理內存,一個一個mspan 處理
當開始運行程序時声滥,Go 將設置一個后臺運行的Worker(唯一的任務就是去清理內存)眉撵,它將進入睡眠狀態(tài)并等待內存段掃描
當GC worker未清理完內存,但新一輪GC又開始了落塑。這時這個運行 GC 的 goroutine 就會在開始標記階段前去協(xié)助完成剩余的清理工作 - 當申請分配內存時候 lazy 觸發(fā)
當應用程序 goroutine 嘗試在堆內存中分配新內存時纽疟,會觸發(fā)該操作。清理導致的延遲和吞吐量降低被分散到每次內存分配時憾赁。
該方式屬于即時執(zhí)行污朽,由于被使用的內存段已經(jīng)被分發(fā)到每一個P 的本地緩存 mcache 中,很難追蹤首先清理哪些內存龙考,因此Go 會先將所有內存段移動到mcentral膘壶,讓本地緩存mcache 再次請求它們,去即時清理洲愤。即時掃描確保所有內存段都會得到清理(節(jié)省資源)颓芭,同時不會阻塞程序執(zhí)行
Referencs:
https://www.yuque.com/aceld/golang/zhzanb
https://www.cnblogs.com/zj420255586/p/14261834.html#12-%E6%A0%87%E8%AE%B0-%E6%B8%85%E9%99%A4
https://golang.design/under-the-hood/zh-cn/part2runtime/ch08gc/basic/
https://www.qycn.com/xzx/article/10803.html
https://www.ardanlabs.com/blog/2018/12/garbage-collection-in-go-part1-semantics.html
https://www.ardanlabs.com/blog/2018/12/garbage-collection-in-go-part2-semantics.html
https://www.ardanlabs.com/blog/2018/12/garbage-collection-in-go-part3-semantics.html