Golang采用了三色標(biāo)記法來(lái)進(jìn)行垃圾回收,那么在什么場(chǎng)景下會(huì)觸發(fā)這個(gè)回收動(dòng)作呢?
創(chuàng)新互聯(lián)公司專(zhuān)注于企業(yè)成都全網(wǎng)營(yíng)銷(xiāo)、網(wǎng)站重做改版、昌江黎族網(wǎng)站定制設(shè)計(jì)、自適應(yīng)品牌網(wǎng)站建設(shè)、成都h5網(wǎng)站建設(shè)、電子商務(wù)商城網(wǎng)站建設(shè)、集團(tuán)公司官網(wǎng)建設(shè)、成都外貿(mào)網(wǎng)站建設(shè)公司、高端網(wǎng)站制作、響應(yīng)式網(wǎng)頁(yè)設(shè)計(jì)等建站業(yè)務(wù),價(jià)格優(yōu)惠性?xún)r(jià)比高,為昌江黎族等各大城市提供網(wǎng)站開(kāi)發(fā)制作服務(wù)。
源碼主要位于文件 src/runtime/mgc.go go version 1.16
觸發(fā)條件從大方面說(shuō),可分為 手動(dòng)觸發(fā) 和 系統(tǒng)觸發(fā) 兩種方式。手動(dòng)觸發(fā)一般很少用,主要由開(kāi)發(fā)者通過(guò)調(diào)用 runtime.GC() 函數(shù)來(lái)實(shí)現(xiàn),而對(duì)于系統(tǒng)自動(dòng)觸發(fā)是 運(yùn)行時(shí) 根據(jù)一些條件判斷來(lái)進(jìn)行的,這也正是本文要介紹的內(nèi)容。
不管哪種觸發(fā)方式,底層回收機(jī)制是一樣的,所以我們先看一下手動(dòng)觸發(fā),根據(jù)它來(lái)找系統(tǒng)觸發(fā)的條件。
可以看到開(kāi)始執(zhí)行GC的是 gcStart() 函數(shù),它有一個(gè) gcTrigger 參數(shù),是一個(gè)觸發(fā)條件結(jié)構(gòu)體,它的結(jié)構(gòu)體也很簡(jiǎn)單。
其實(shí)在Golang 內(nèi)部所有的GC都是通過(guò) gcStart() 函數(shù),然后指定一個(gè) gcTrigger 的參數(shù)來(lái)開(kāi)始的,而手動(dòng)觸發(fā)指定的條件值為 gcTriggerCycle 。 gcStart 是一個(gè)很復(fù)雜的函數(shù),有興趣的可以看一下源碼實(shí)現(xiàn)。
對(duì)于 kind 的值有三種,分別為 gcTriggerHeap 、 gcTriggerTime 和 gcTriggerCycle 。
運(yùn)行時(shí)會(huì)通過(guò) gcTrigger.test() 函數(shù)來(lái)決定是否需要觸發(fā)GC,只要滿(mǎn)足上面基中一個(gè)即可。
到此我們基本明白了這三種觸發(fā)GC的條件,那么對(duì)于系統(tǒng)自動(dòng)觸發(fā)這種,Golang 從一個(gè)程序的開(kāi)始到運(yùn)行,它又是如何一步一步監(jiān)控到這個(gè)條件的呢?
其實(shí) runtime 在程序啟動(dòng)時(shí),會(huì)在一個(gè)初始化函數(shù) init() 里啟用一個(gè) forcegchelper() 函數(shù),這個(gè)函數(shù)位于 proc.go 文件。
為了減少系統(tǒng)資源占用,在 forcegchelper 函數(shù)里會(huì)通過(guò) goparkunlock() 函數(shù)主動(dòng)讓自己陷入休眠,以后由 sysmon() 監(jiān)控線(xiàn)程根據(jù)條件來(lái)恢復(fù)這個(gè)gc goroutine。
可以看到 sysmon() 會(huì)在一個(gè) for 語(yǔ)句里一直判斷這個(gè) gcTriggerTime 這個(gè)條件是否滿(mǎn)足,如果滿(mǎn)足的話(huà),會(huì)將 forcegc.g 這個(gè) goroutine 添加到全局隊(duì)列里進(jìn)行調(diào)度(這里 forcegc 是一個(gè)全局變量)。
調(diào)度器在調(diào)度循環(huán) runtime.schedule 中還可以通過(guò)垃圾收集控制器的 runtime.gcControllerState.findRunnabledGCWorker 獲取并執(zhí)行用于后臺(tái)標(biāo)記的任務(wù)。
gc 與gccgo 都是go語(yǔ)言標(biāo)準(zhǔn)規(guī)范的不同實(shí)現(xiàn),兩者包含不同的側(cè)重點(diǎn):
使用成本上gccgo遠(yuǎn)比gc更高,基于如下原因:
總結(jié):除非真要追求高性能,否則不建議去折騰gccgo
如果一定要折騰,建議思路:基于gcc docker 鏡像,編寫(xiě)Dockerfile,安裝golang,然后使用 go build -compiler=gccgo 。
相關(guān)資源:
GC 與 mutator 線(xiàn)程并發(fā)運(yùn)行,允許多個(gè) GC 線(xiàn)程并行運(yùn)行
GC 是一個(gè)使用寫(xiě)屏障的并發(fā)標(biāo)記和清除。
GC 是非分代的,非緊湊的。
Allocation 是按照大小隔離每個(gè) P 分配的區(qū)域來(lái)完成的,以在消除常見(jiàn)情況下的鎖的同時(shí),最小化碎片。
了解 GC 的好地方,可以從 Richard Jones 的 gchandbook.org 開(kāi)始。
1. GC 執(zhí)行清除終止
? a. Stop the world ,這將導(dǎo)致所有 P 達(dá)到 GC 安全點(diǎn)。
? b. 清除任何未清除過(guò)的 spans ,只有在預(yù)期時(shí)間之前強(qiáng)制執(zhí)行此 GC 周期時(shí),才會(huì)有未清除的 span 。
2. GC 執(zhí)行標(biāo)記階段
? a.?? 準(zhǔn)備標(biāo)記階段,將 gcphase 設(shè)置為 _GCmark (從 _GCoff 開(kāi)始),啟用寫(xiě)屏障,啟用 mutator assist ,并對(duì)根標(biāo)記作業(yè)進(jìn)行排隊(duì)。
在所有 P 都啟用寫(xiě)屏障之前,不會(huì)掃描任何對(duì)象,這是使用 STW 完成的。
? ?b. Start the world ,從現(xiàn)在開(kāi)始,GC 工作由調(diào)度器啟動(dòng)的 標(biāo)記worker 和作 為 allocation 的一部分執(zhí)行的 assists 來(lái)完成。
寫(xiě)屏障將覆寫(xiě)的指針和任何指針寫(xiě)的新指針值都著色。
新分配的對(duì)象立即被標(biāo)記為黑色。
? c.?? GC 執(zhí)行根標(biāo)記作業(yè)。包括: 掃描所有棧 , 著色所有全局變量 ,以及 著色堆外運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)中的任何堆指針 。
掃描棧會(huì)停止goroutine,對(duì)goroutine棧中找到的任何指針進(jìn)行著色,然后恢復(fù)goroutine。
? ? d.?? GC 耗盡灰色對(duì)象的工作隊(duì)列,將每個(gè) 灰色 對(duì)象掃描為 黑色 ,并對(duì)在該對(duì)象中找到的所有指針進(jìn)行著色(反過(guò)來(lái)可能會(huì)將這些指針添加到工作隊(duì)列中)。
? ?e.?? 由于 GC work 分散在本地緩存中,因此 GC 使用 分布式終止算法 來(lái)檢測(cè)何時(shí)不再有根標(biāo)記作業(yè)或灰色對(duì)象(參見(jiàn) gcMarkDone 函數(shù))。
此時(shí),GC 狀態(tài)轉(zhuǎn)換到標(biāo)記終止( gcMarkTermination )。
3. GC 執(zhí)行標(biāo)記終止 gcMarkTermination
? a. Stop the world
? b. 將 gcphase 設(shè)置為 _GCmarktermination ,并禁用 workers 和 assists。
? c. 進(jìn)行內(nèi)務(wù)整理,如 flushing mcaches
4. GC 執(zhí)行清除階段
? ?a. 準(zhǔn)備清除階段,將 gcphase 設(shè)置為 _GCoff ,設(shè)置清除狀態(tài)并禁用寫(xiě)屏障。
? b. Start the world ,從現(xiàn)在開(kāi)始,新分配的對(duì)象是白色的,如有必要,在使用 spans 前 allocating 清除 spans 。
? ?c. GC 在后臺(tái)進(jìn)行 并發(fā)清除 并響應(yīng) allocation ,見(jiàn)下面的描述。
5. 當(dāng)分配足夠時(shí),重復(fù)上面 1 開(kāi)始的步驟,參見(jiàn)下面關(guān)于 GC rate 的討論。
清除階段與正常程序執(zhí)行并發(fā)進(jìn)行。
在后臺(tái) goroutine 中,堆被惰性(當(dāng) goroutine 需要另一個(gè) span 時(shí))且并發(fā)地逐個(gè) span 掃描(這有助于不是 CPU bound 的程序)。
在 STW 標(biāo)記終止 的結(jié)尾,所有的 span 都被標(biāo)記為 需要清除 。
后臺(tái)清除器 goroutine 簡(jiǎn)單地逐個(gè)清除 span 。
為了避免在存在未清除的 span 時(shí)請(qǐng)求更多的 OS內(nèi)存 ,當(dāng) goroutine 需要另一個(gè) span 時(shí),它首先嘗試通過(guò)清除來(lái)回收這些內(nèi)存。
當(dāng) goroutine 需要分配一個(gè)新的 小對(duì)象span 時(shí),它會(huì)清除相同大小的小對(duì)象 span ,直到釋放至少一個(gè)對(duì)象為止。
當(dāng) goroutine 需要從堆中分配 大對(duì)象span 時(shí),它會(huì)清除 span ,直到將至少那么多頁(yè)面釋放到堆中。
有一種情況,這可能是不夠的:如果 goroutine 清除并釋放兩個(gè)不相鄰的 單頁(yè)span 到堆中,那么它將分配一個(gè)新的 雙頁(yè)span ,但是仍然可以有其他 單頁(yè)未清除的span ,可以組合成 雙頁(yè)的span 。
確保在未清除的 span 上不進(jìn)行任何操作(這會(huì)破壞 GC 位圖中的標(biāo)記位)至關(guān)重要。
在 GC 期間,所有 mcache 都被刷新到 中央緩存 中,因此它們是空的。
當(dāng)一個(gè) goroutine 抓取一個(gè)新的 span 到 mcache 時(shí), goroutine 會(huì)清除 mcache 。
當(dāng) goroutine 顯式釋放對(duì)象或設(shè)置 finalizer 時(shí),goroutine 確保 span 已經(jīng)清除(通過(guò)清除或者等待并發(fā)清除完成)。
finalizer goroutine 僅在所有 span 已經(jīng)清除時(shí)才開(kāi)始。
當(dāng)下一次 GC 啟動(dòng)時(shí),它將清除所有尚未清除的 span (如果有的話(huà))。
下一次 GC 是在我們分配了與已經(jīng)使用的內(nèi)存成正比的額外內(nèi)存量之后。
該比例由 GOGC 環(huán)境變量控制(默認(rèn)為 100 )。
如果 GOGC=100 ,而我們使用的是 4M ,那么當(dāng)達(dá)到 8M 時(shí),我們將再次進(jìn)行 GC(此標(biāo)記在 next_gc 變量中被跟蹤)。
獲取 GOGC :
這使得 GC成本 與 allocation 成本 成線(xiàn)性比例。
調(diào)整 GOGC 只會(huì)改變線(xiàn)性常量(以及使用的額外內(nèi)存量)。
為了防止在掃描大型對(duì)象時(shí)出現(xiàn)長(zhǎng)時(shí)間的暫停,并提高并行性,垃圾收集器將大于 maxObletBytes 的對(duì)象的掃描作業(yè)分解為最多 maxObletBytes 的 oblets 。
當(dāng)掃描遇到大對(duì)象時(shí),它只掃描第一個(gè) oblet ,并將其余 oblets 作為新的掃描作業(yè)排隊(duì)。