新聞中心
Golang實(shí)驗(yàn)性功能SetMaxHeap 固定值GC
簡(jiǎn)單來(lái)說(shuō), SetMaxHeap 提供了一種可以設(shè)置固定觸發(fā)閾值的 GC (Garbage Collection垃圾回收)方式
創(chuàng)新互聯(lián)建站專(zhuān)注于企業(yè)全網(wǎng)營(yíng)銷(xiāo)推廣、網(wǎng)站重做改版、曲麻萊網(wǎng)站定制設(shè)計(jì)、自適應(yīng)品牌網(wǎng)站建設(shè)、HTML5、電子商務(wù)商城網(wǎng)站建設(shè)、集團(tuán)公司官網(wǎng)建設(shè)、外貿(mào)營(yíng)銷(xiā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ù)。
官方源碼鏈接
大量臨時(shí)對(duì)象分配導(dǎo)致的 GC 觸發(fā)頻率過(guò)高, GC 后實(shí)際存活的對(duì)象較少,
或者機(jī)器內(nèi)存較充足,希望使用剩余內(nèi)存,降低 GC 頻率的場(chǎng)景
GC 會(huì) STW ( Stop The World ),對(duì)于時(shí)延敏感場(chǎng)景,在一個(gè)周期內(nèi)連續(xù)觸發(fā)兩輪 GC ,那么 STW 和 GC 占用的 CPU 資源都會(huì)造成很大的影響, SetMaxHeap 并不一定是完美的,在某些場(chǎng)景下做了些權(quán)衡,官方也在進(jìn)行相關(guān)的實(shí)驗(yàn),當(dāng)前方案仍沒(méi)有合入主版本。
先看下如果沒(méi)有 SetMaxHeap ,對(duì)于如上所述的場(chǎng)景的解決方案
這里簡(jiǎn)單說(shuō)下 GC 的幾個(gè)值的含義,可通過(guò) GODEBUG=gctrace=1 獲得如下數(shù)據(jù)
這里只關(guān)注 128-132-67 MB 135 MB goal ,
分別為 GC開(kāi)始時(shí)內(nèi)存使用量 - GC標(biāo)記完成時(shí)內(nèi)存使用量 - GC標(biāo)記完成時(shí)的存活內(nèi)存量 本輪GC標(biāo)記完成時(shí)的 預(yù)期 內(nèi)存使用量(上一輪 GC 完成時(shí)確定)
引用 GC peace設(shè)計(jì)文檔 中的一張圖來(lái)說(shuō)明
對(duì)應(yīng)關(guān)系如下:
簡(jiǎn)單說(shuō)下 GC pacing (信用機(jī)制)
GC pacing 有兩個(gè)目標(biāo),
那么當(dāng)一輪 GC 完成時(shí),如何只根據(jù)本輪 GC 存活量去實(shí)現(xiàn)這兩個(gè)小目標(biāo)呢?
這里實(shí)際是根據(jù)當(dāng)前的一些數(shù)據(jù)或狀態(tài)去 預(yù)估 “未來(lái)”,所有會(huì)存在些誤差
首先確定 gc Goal goal = memstats.heap_marked + memstats.heap_marked*uint64(gcpercent)/100
heap_marked 為本輪 GC 存活量, gcpercent 默認(rèn)為 100 ,可以通過(guò)環(huán)境變量 GOGC=100 或者 debug.SetGCPercent(100) 來(lái)設(shè)置
那么默認(rèn)情況下 goal = 2 * heap_marked
gc_trigger 是與 goal 相關(guān)的一個(gè)值( gc_trigger 大約為 goal 的 90% 左右),每輪 GC 標(biāo)記完成時(shí),會(huì)根據(jù) |Ha-Hg| 和實(shí)際使用的 cpu 資源 動(dòng)態(tài)調(diào)整 gc_trigger 與 goal 的差值
goal 與 gc_trigger 的差值即為,為 GC 期間分配的對(duì)象所預(yù)留的空間
GC pacing 還會(huì)預(yù)估下一輪 GC 發(fā)生時(shí),需要掃描對(duì)象對(duì)象的總量,進(jìn)而換算為下一輪 GC 所需的工作量,進(jìn)而計(jì)算出 mark assist 的值
本輪 GC 觸發(fā)( gc_trigger ),到本輪的 goal 期間,需要盡力完成 GC mark 標(biāo)記操作,所以當(dāng) GC 期間,某個(gè) goroutine 分配大量?jī)?nèi)存時(shí),就會(huì)被拉去做 mark assist 工作,先進(jìn)行 GC mark 標(biāo)記賺取足夠的信用值后,才能分配對(duì)應(yīng)大小的對(duì)象
根據(jù)本輪 GC 存活的內(nèi)存量( heap_marked )和下一輪 GC 觸發(fā)的閾值( gc_trigger )計(jì)算 sweep assist 的值,本輪 GC 完成,到下一輪 GC 觸發(fā)( gc_trigger )時(shí),需要盡力完成 sweep 清掃操作
預(yù)估下一輪 GC 所需的工作量的方式如下:
繼續(xù)分析文章開(kāi)頭的問(wèn)題,如何充分利用剩余內(nèi)存,降低 GC 頻率和 GC 對(duì) CPU 的資源消耗
如上圖可以看出, GC 后,存活的對(duì)象為 2GB 左右,如果將 gcpercent 設(shè)置為 400 ,那么就可以將下一輪 GC 觸發(fā)閾值提升到 10GB 左右
前面一輪看起來(lái)很好,提升了 GC 觸發(fā)的閾值到 10GB ,但是如果某一輪 GC 后的存活對(duì)象到達(dá) 2.5GB 的時(shí)候,那么下一輪 GC 觸發(fā)的閾值,將會(huì)超過(guò)內(nèi)存閾值,造成 OOM ( Out of Memory ),進(jìn)而導(dǎo)致程序崩潰。
可以通過(guò) GOGC=off 或者 debug.SetGCPercent(-1) 來(lái)關(guān)閉 GC
可以通過(guò)進(jìn)程外監(jiān)控內(nèi)存使用狀態(tài),使用信號(hào)觸發(fā)的方式通知程序,或 ReadMemStats 、或 linkname runtime.heapRetained 等方式進(jìn)行堆內(nèi)存使用的監(jiān)測(cè)
可以通過(guò)調(diào)用 runtime.GC() 或者 debug.FreeOSMemory() 來(lái)手動(dòng)進(jìn)行 GC 。
這里還需要說(shuō)幾個(gè)事情來(lái)解釋這個(gè)方案所存在的問(wèn)題
通過(guò) GOGC=off 或者 debug.SetGCPercent(-1) 是如何關(guān)閉 GC 的?
gc 4 @1.006s 0%: 0.033+5.6+0.024 ms clock, 0.27+4.4/11/25+0.19 ms cpu, 428-428-16 MB, 17592186044415 MB goal, 8 P (forced)
通過(guò) GC trace 可以看出,上面所說(shuō)的 goal 變成了一個(gè)很詭異的值 17592186044415
實(shí)際上關(guān)閉 GC 后, Go 會(huì)將 goal 設(shè)置為一個(gè)極大值 ^uint64(0) ,那么對(duì)應(yīng)的 GC 觸發(fā)閾值也被調(diào)成了一個(gè)極大值,這種處理方式看起來(lái)也沒(méi)什么問(wèn)題,將閾值調(diào)大,預(yù)期永遠(yuǎn)不會(huì)再觸發(fā) GC
那么如果在關(guān)閉 GC 的情況下,手動(dòng)調(diào)用 runtime.GC() 會(huì)導(dǎo)致什么呢?
由于 goal 和 gc_trigger 被設(shè)置成了極大值, mark assist 和 sweep assist 也會(huì)按照這個(gè)錯(cuò)誤的值去計(jì)算,導(dǎo)致工作量預(yù)估錯(cuò)誤,這一點(diǎn)可以從 trace 中進(jìn)行證明
可以看到很詭異的 trace 圖,這里不做深究,該方案與 GC pacing 信用機(jī)制不兼容
記住,不要在關(guān)閉 GC 的情況下手動(dòng)觸發(fā) GC ,至少在當(dāng)前 Go1.14 版本中仍存在這個(gè)問(wèn)題
SetMaxHeap 的實(shí)現(xiàn)原理,簡(jiǎn)單來(lái)說(shuō)是強(qiáng)行控制了 goal 的值
注: SetMaxHeap ,本質(zhì)上是一個(gè)軟限制,并不能解決 極端場(chǎng)景 下的 OOM ,可以配合內(nèi)存監(jiān)控和 debug.FreeOSMemory() 使用
SetMaxHeap 控制的是堆內(nèi)存大小, Go 中除了堆內(nèi)存還分配了如下內(nèi)存,所以實(shí)際使用過(guò)程中,與實(shí)際硬件內(nèi)存閾值之間需要留有一部分余量。
對(duì)于文章開(kāi)始所述問(wèn)題,使用 SetMaxHeap 后,預(yù)期的 GC 過(guò)程大概是這個(gè)樣子
簡(jiǎn)單用法1
該方法簡(jiǎn)單粗暴,直接將 goal 設(shè)置為了固定值
注:通過(guò)上文所講,觸發(fā) GC 實(shí)際上是 gc_trigger ,所以當(dāng)閾值設(shè)置為 12GB 時(shí),會(huì)提前一點(diǎn)觸發(fā) GC ,這里為了描述方便,近似認(rèn)為 gc_trigger=goal
簡(jiǎn)單用法2
當(dāng)不關(guān)閉 GC 時(shí), SetMaxHeap 的邏輯是, goal 仍按照 gcpercent 進(jìn)行計(jì)算,當(dāng) goal 小于 SetMaxHeap 閾值時(shí)不進(jìn)行處理;當(dāng) goal 大于 SetMaxHeap 閾值時(shí),將 goal 限制為 SetMaxHeap 閾值
注:通過(guò)上文所講,觸發(fā) GC 實(shí)際上是 gc_trigger ,所以當(dāng)閾值設(shè)置為 12GB 時(shí),會(huì)提前一點(diǎn)觸發(fā) GC ,這里為了描述方便,近似認(rèn)為 gc_trigger=goal
切換到 go1.14 分支,作者選擇了 git checkout go1.14.5
選擇官方提供的 cherry-pick 方式(可能需要梯子,文件改動(dòng)不多,我后面會(huì)列出具體改動(dòng))
git fetch "" refs/changes/67/227767/3 git cherry-pick FETCH_HEAD
需要重新編譯Go源碼
注意點(diǎn):
下面源碼中的官方注釋說(shuō)的比較清楚,在一些關(guān)鍵位置加入了中文注釋
入?yún)ytes為要設(shè)置的閾值
notify 簡(jiǎn)單理解為 GC 的策略 發(fā)生變化時(shí)會(huì)向 channel 發(fā)送通知,后續(xù)源碼可以看出“策略”具體指哪些內(nèi)容
返回值為本次設(shè)置之前的 MaxHeap 值
$GOROOT/src/runtime/debug/garbage.go
$GOROOT/src/runtime/mgc.go
注:作者盡量用通俗易懂的語(yǔ)言去解釋 Go 的一些機(jī)制和 SetMaxHeap 功能,可能有些描述與實(shí)現(xiàn)細(xì)節(jié)不完全一致,如有錯(cuò)誤還請(qǐng)指出
Go 語(yǔ)言三色標(biāo)記掃描對(duì)象是 DFS 還是 BFS?
最近在看左神新書(shū) 《Go 語(yǔ)言設(shè)計(jì)與實(shí)現(xiàn)》的垃圾收集器時(shí)產(chǎn)生一個(gè)疑惑,花了點(diǎn)時(shí)間搞清楚了記錄一下。
Go 語(yǔ)言垃圾回收的實(shí)現(xiàn)使用了標(biāo)記清除算法,將對(duì)象的狀態(tài)抽象成黑色(活躍對(duì)象)、灰色(活躍對(duì)象中間狀態(tài))、白色(潛在垃圾對(duì)象也是所有對(duì)象的默認(rèn)狀態(tài))三種,注意沒(méi)有具體的字段標(biāo)記顏色。
整個(gè)標(biāo)記過(guò)程就是把白色對(duì)象標(biāo)黑的過(guò)程:
1.首先將 ROOT 根對(duì)象(包括全局變量、goroutine 棧上的對(duì)象等)放入到灰色集合
2.選一個(gè)灰色對(duì)象,標(biāo)成黑色,將所有可達(dá)的子對(duì)象放入到灰色集合
3.重復(fù)2的步驟,直到灰色集合中為空
下圖是書(shū)上的插圖,看上去是一個(gè)典型的深度優(yōu)先搜索的算法。
下圖是劉丹冰寫(xiě)的《Golang 修養(yǎng)之路》的插圖,看上去是一個(gè)典型的廣度優(yōu)先搜索的算法。
我疑惑的點(diǎn)在于這個(gè)標(biāo)記過(guò)程是深度優(yōu)先算法還是廣度優(yōu)先算法,因?yàn)楹芏辔恼虏┛蛯?duì)此都沒(méi)有很清楚的說(shuō)明,作為學(xué)習(xí)者這種細(xì)節(jié)其實(shí)也不影響對(duì)整個(gè) GC 流程的理解,但是這種細(xì)節(jié)我非常喜歡扣:)
對(duì)著書(shū)和源碼摸索著大致找到了一個(gè)結(jié)果是深度優(yōu)先。下面看下大致的過(guò)程,源碼基于1.15.2版本:
gcStart 是 Go 語(yǔ)言三種條件觸發(fā) GC 的共同入口
啟動(dòng)后臺(tái)標(biāo)記任務(wù)
為每個(gè)處理器創(chuàng)建用于執(zhí)行后臺(tái)標(biāo)記任務(wù)的 Goroutine
上面休眠的 G 會(huì)在調(diào)度循環(huán)中檢查并喚醒執(zhí)行
執(zhí)行標(biāo)記
gcw 是每個(gè) P 獨(dú)有的所以不用擔(dān)心并發(fā)的問(wèn)題 和 GMP、mcache 一樣設(shè)計(jì),減少鎖競(jìng)爭(zhēng)
嘗試在全局列表中獲取一個(gè)不為空的 buf
這是官方實(shí)現(xiàn)的無(wú)鎖隊(duì)列:)漲見(jiàn)識(shí)了,for 循環(huán)加原子操作實(shí)現(xiàn)棧的 pop
到這里從灰色集合中獲取待掃描的對(duì)象邏輯說(shuō)完了。找到對(duì)象了接著就是 scanobject(b, gcw) 了,里面有兩段邏輯要注意
根據(jù)索引位置找到對(duì)象進(jìn)行標(biāo)色
嘗試存入 gcwork 的緩存中,或全局隊(duì)列中
無(wú)鎖隊(duì)列,for 循環(huán)加原子操作實(shí)現(xiàn)棧的 push
到這里把灰色對(duì)象標(biāo)黑就完成了,又放回灰色集合接著掃下一個(gè)指針。
Go 語(yǔ)言設(shè)計(jì)與實(shí)現(xiàn) 垃圾收集器
Golang三色標(biāo)記+混合寫(xiě)屏障GC模式全分析
【golang】?jī)?nèi)存逃逸常見(jiàn)情況和避免方式
因?yàn)槿绻兞康膬?nèi)存發(fā)生逃逸,它的生命周期就是不可知的,其會(huì)被分配到堆上,而堆上分配內(nèi)存不能像棧一樣會(huì)自動(dòng)釋放,為了解放程序員雙手,專(zhuān)注于業(yè)務(wù)的實(shí)現(xiàn),go實(shí)現(xiàn)了gc垃圾回收機(jī)制,但gc會(huì)影響程序運(yùn)行性能,所以要盡量減少程序的gc操作。
1、在方法內(nèi)把局部變量指針?lè)祷?,被外部引用,其生命周期大于棧,則溢出。
2、發(fā)送指針或帶有指針的值到channel,因?yàn)榫幾g時(shí)候無(wú)法知道那個(gè)goroutine會(huì)在channel接受數(shù)據(jù),編譯器無(wú)法知道什么時(shí)候釋放。
3、在一個(gè)切片上存儲(chǔ)指針或帶指針的值。比如[]*string,導(dǎo)致切片內(nèi)容逃逸,其引用值一直在堆上。
4、因?yàn)榍衅腶ppend導(dǎo)致超出容量,切片重新分配地址,切片背后的存儲(chǔ)基于運(yùn)行時(shí)的數(shù)據(jù)進(jìn)行擴(kuò)充,就會(huì)在堆上分配。
5、在interface類(lèi)型上調(diào)用方法,在Interface調(diào)用方法是動(dòng)態(tài)調(diào)度的,只有在運(yùn)行時(shí)才知道。
1、go語(yǔ)言的接口類(lèi)型方法調(diào)用是動(dòng)態(tài),因此不能在編譯階段確定,所有類(lèi)型結(jié)構(gòu)轉(zhuǎn)換成接口的過(guò)程會(huì)涉及到內(nèi)存逃逸發(fā)生,在頻次訪(fǎng)問(wèn)較高的函數(shù)盡量調(diào)用接口。
2、不要盲目使用變量指針作為參數(shù),雖然減少了復(fù)制,但變量逃逸的開(kāi)銷(xiāo)更大。
3、預(yù)先設(shè)定好slice長(zhǎng)度,避免頻繁超出容量,重新分配。
網(wǎng)站題目:go語(yǔ)言什么情況觸發(fā)gc go語(yǔ)言運(yùn)行時(shí)
鏈接URL:http://fisionsoft.com.cn/article/ddjpcdo.html