Go垃圾回收系列之七:標(biāo)記終止與調(diào)步算法
標(biāo)記終止階段
當(dāng)完成并發(fā)標(biāo)記階段所有灰色對象的掃描和標(biāo)記,則進(jìn)入到標(biāo)記終止階段。標(biāo)記終止階段會再次進(jìn)入STW,標(biāo)記終止階段主要完成一些指標(biāo)例如用時的統(tǒng)計(jì)、統(tǒng)計(jì)強(qiáng)制開始GC的次數(shù)、更新下一次觸發(fā)gc需要達(dá)到的目標(biāo)、關(guān)閉寫屏障、并喚醒后臺清掃的協(xié)程,開始下一階段的清掃工作。
在標(biāo)記終止階段重要的任務(wù)是計(jì)算下一次觸發(fā)垃圾回收時需要達(dá)到的堆目標(biāo),這叫做垃圾回收的調(diào)步算法。
調(diào)步算法是Go1.5提出的算法,由于Go1.5開始使用并發(fā)的三色標(biāo)記,因此,當(dāng)開始進(jìn)行GC到GC結(jié)束的過程中,用戶協(xié)程也可能在分配大量的內(nèi)存。所以在GC的過程中,內(nèi)存的大小實(shí)際上超過了我們設(shè)定的觸發(fā)GC的目標(biāo)。為了解決這樣的問題,需要對程序進(jìn)行估計(jì),從而在目標(biāo)內(nèi)存之前就啟動GC、并預(yù)計(jì)在GC結(jié)束之后,程序的內(nèi)存大小剛好在目標(biāo)內(nèi)存附近。

因此,調(diào)步算法最重要的任務(wù)是估計(jì)出最佳的下一次觸發(fā)GC的時機(jī),而這依賴于本次GC階段差額最終GC完成后的內(nèi)存與目標(biāo)內(nèi)存之前的差距。當(dāng)GC完成后的內(nèi)存遠(yuǎn)小于目標(biāo)內(nèi)存,意味著我們觸發(fā)GC的時間過早。如果GC完成后的內(nèi)存遠(yuǎn)大于目標(biāo)內(nèi)存,意味著觸發(fā)GC的時間太遲。
因此調(diào)度算法 的第一個目標(biāo)是min(|目前內(nèi)存-本次GC完成標(biāo)記后的內(nèi)存|),除此之外,調(diào)步算法還有第二個目標(biāo),即預(yù)計(jì)執(zhí)行標(biāo)記的CPU暫用率接近25%。結(jié)合之前提到的25%的后臺標(biāo)記協(xié)程,這個目標(biāo)是滿足的,正常情況下,只會有25%的CPU去執(zhí)行后臺標(biāo)記任務(wù)。但是當(dāng)用戶工作協(xié)程執(zhí)行了“輔助標(biāo)記”
(下一篇文章介紹),這一目標(biāo)將不再成立。當(dāng)用戶協(xié)程執(zhí)行了過多的輔助標(biāo)記,這將導(dǎo)致GC標(biāo)記完成后的內(nèi)存偏小,因?yàn)橛脩魠f(xié)程本來應(yīng)該分配內(nèi)存的時間用來了執(zhí)行輔助標(biāo)記。
算法將首先計(jì)算目標(biāo)內(nèi)存與實(shí)際內(nèi)存的偏差,這是通過計(jì)算
偏差率 = (目標(biāo)增長率 - 觸發(fā)增率) - (實(shí)際增長率 - 觸發(fā)率)來實(shí)現(xiàn)的。這其實(shí)是偏差=(目標(biāo)內(nèi)存 - 觸發(fā)GC時的內(nèi)存) - (GC標(biāo)記完成后的內(nèi)存- 觸發(fā)GC時的內(nèi)存) 的變形。
為了修復(fù)輔助標(biāo)記帶來的偏差,計(jì)算了輔助標(biāo)記所用的時間,從而調(diào)整了(GC標(biāo)記完成后的內(nèi)存- 觸發(fā)GC時的內(nèi)存) 的大小。因此最終的偏差會調(diào)整為:
偏差率 = (目標(biāo)增長率 - 觸發(fā)率) - 調(diào)整率 *(實(shí)際增長率 - 觸發(fā)率)實(shí)際代碼如下:
func (c *gcControllerState) endCycle() float64 {
utilization := gcBackgroundUtilization
if assistDuration > 0 {
utilization += float64(c.assistTime) / float64(assistDuration*int64(gomaxprocs))
}
triggerError := goalGrowthRatio - memstats.triggerRatio - utilization/gcGoalUtilization*(actualGrowthRatio-memstats.triggerRatio)
triggerRatio := memstats.triggerRatio + triggerGain*triggerError
}從公式中可以看出,實(shí)際增長率 和 輔助標(biāo)記的時間都會影響最終的偏差。當(dāng)調(diào)整后估計(jì)的實(shí)際內(nèi)存越偏離于實(shí)際內(nèi)存時,偏差率越大。這時,會調(diào)整下一次GC時的觸發(fā)率,調(diào)整時,采取了漸進(jìn)調(diào)整,每次只調(diào)整偏差的一半。下一次GC時的觸發(fā)率的公式如下:
下次GC觸發(fā)率 = 上次GC觸發(fā)率 + 1/2 * 偏差率計(jì)算完GC觸發(fā)率之后,最終需要計(jì)算出實(shí)際的GC觸發(fā)大小。這是在標(biāo)記終止階段gcSetTriggerRatio函數(shù)中完成的。目標(biāo)內(nèi)存的大小計(jì)算為:
goal := ^uint64(0)
if gcpercent >= 0 {
goal = memstats.heap_marked + memstats.heap_marked*uint64(gcpercent)/100
}goal為下次GC完成后期望的目標(biāo)內(nèi)存,其取決于本次GC中掃描后內(nèi)存的大小以及gcpercent的大小。gcpercent是可以由用戶動態(tài)設(shè)置的。調(diào)用debug標(biāo)準(zhǔn)庫的SetGCPercent函數(shù),可以修改gcpercent的大小。
func SetGCPercent(percent int) intgcpercent默認(rèn)為100,代表目標(biāo)內(nèi)存是前一此GC標(biāo)記內(nèi)存的一倍。當(dāng)修改gcpercent小于0,將禁用Go的垃圾回收。另外,也可以在編譯或運(yùn)行時添加"GOGC"環(huán)境變量的方式修改gcpercent大小。核心邏輯是在程序初始化時調(diào)用readgogc()實(shí)現(xiàn)的。例如GOGC=off ./main 將關(guān)閉程序的GC回收。
func readgogc() int32 {
p := gogetenv("GOGC")
if p == "off" {
return -1
}
if n, ok := atoi32(p); ok {
return n
}
return 100
}當(dāng)明確了目標(biāo)內(nèi)存大小,觸發(fā)內(nèi)存的大小可以簡單定義為觸發(fā)內(nèi)存的大小 = 觸發(fā)率 * 目標(biāo)內(nèi)存大小,但觸發(fā)率不能超過0.95,也不能夠小于0.6。因此最終觸發(fā)內(nèi)存大小在0.6*目標(biāo)內(nèi)存大小—0.95*目標(biāo)內(nèi)存大小之間。
推薦閱讀
