新聞中心
Golang實(shí)驗(yàn)性功能SetMaxHeap 固定值GC
簡單來說, SetMaxHeap 提供了一種可以設(shè)置固定觸發(fā)閾值的 GC (Garbage Collection垃圾回收)方式
十年的新區(qū)網(wǎng)站建設(shè)經(jīng)驗(yàn),針對(duì)設(shè)計(jì)、前端、開發(fā)、售后、文案、推廣等六對(duì)一服務(wù),響應(yīng)快,48小時(shí)及時(shí)工作處理。營銷型網(wǎng)站建設(shè)的優(yōu)勢(shì)是能夠根據(jù)用戶設(shè)備顯示端的尺寸不同,自動(dòng)調(diào)整新區(qū)建站的顯示方式,使網(wǎng)站能夠適用不同顯示終端,在瀏覽器中調(diào)整網(wǎng)站的寬度,無論在任何一種瀏覽器上瀏覽網(wǎng)站,都能展現(xiàn)優(yōu)雅布局與設(shè)計(jì),從而大程度地提升瀏覽體驗(yàn)。創(chuàng)新互聯(lián)從事“新區(qū)網(wǎng)站設(shè)計(jì)”,“新區(qū)網(wǎng)站推廣”以來,每個(gè)客戶項(xiàng)目都認(rèn)真落實(shí)執(zhí)行。
官方源碼鏈接
大量臨時(shí)對(duì)象分配導(dǎo)致的 GC 觸發(fā)頻率過高, 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)前方案仍沒有合入主版本。
先看下如果沒有 SetMaxHeap ,對(duì)于如上所述的場(chǎng)景的解決方案
這里簡單說下 GC 的幾個(gè)值的含義,可通過 GODEBUG=gctrace=1 獲得如下數(shù)據(jù)
這里只關(guān)注 128-132-67 MB 135 MB goal ,
分別為 GC開始時(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ì)文檔 中的一張圖來說明
對(duì)應(yīng)關(guān)系如下:
簡單說下 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ù)估 “未來”,所有會(huì)存在些誤差
首先確定 gc Goal goal = memstats.heap_marked + memstats.heap_marked*uint64(gcpercent)/100
heap_marked 為本輪 GC 存活量, gcpercent 默認(rèn)為 100 ,可以通過環(huán)境變量 GOGC=100 或者 debug.SetGCPercent(100) 來設(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 分配大量內(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ù)分析文章開頭的問題,如何充分利用剩余內(nèi)存,降低 GC 頻率和 GC 對(duì) CPU 的資源消耗
如上圖可以看出, GC 后,存活的對(duì)象為 2GB 左右,如果將 gcpercent 設(shè)置為 400 ,那么就可以將下一輪 GC 觸發(fā)閾值提升到 10GB 左右
前面一輪看起來很好,提升了 GC 觸發(fā)的閾值到 10GB ,但是如果某一輪 GC 后的存活對(duì)象到達(dá) 2.5GB 的時(shí)候,那么下一輪 GC 觸發(fā)的閾值,將會(huì)超過內(nèi)存閾值,造成 OOM ( Out of Memory ),進(jìn)而導(dǎo)致程序崩潰。
可以通過 GOGC=off 或者 debug.SetGCPercent(-1) 來關(guān)閉 GC
可以通過進(jìn)程外監(jiān)控內(nèi)存使用狀態(tài),使用信號(hào)觸發(fā)的方式通知程序,或 ReadMemStats 、或 linkname runtime.heapRetained 等方式進(jìn)行堆內(nèi)存使用的監(jiān)測(cè)
可以通過調(diào)用 runtime.GC() 或者 debug.FreeOSMemory() 來手動(dòng)進(jìn)行 GC 。
這里還需要說幾個(gè)事情來解釋這個(gè)方案所存在的問題
通過 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)
通過 GC trace 可以看出,上面所說的 goal 變成了一個(gè)很詭異的值 17592186044415
實(shí)際上關(guān)閉 GC 后, Go 會(huì)將 goal 設(shè)置為一個(gè)極大值 ^uint64(0) ,那么對(duì)應(yīng)的 GC 觸發(fā)閾值也被調(diào)成了一個(gè)極大值,這種處理方式看起來也沒什么問題,將閾值調(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è)問題
SetMaxHeap 的實(shí)現(xiàn)原理,簡單來說是強(qiáng)行控制了 goal 的值
注: SetMaxHeap ,本質(zhì)上是一個(gè)軟限制,并不能解決 極端場(chǎng)景 下的 OOM ,可以配合內(nèi)存監(jiān)控和 debug.FreeOSMemory() 使用
SetMaxHeap 控制的是堆內(nèi)存大小, Go 中除了堆內(nèi)存還分配了如下內(nèi)存,所以實(shí)際使用過程中,與實(shí)際硬件內(nèi)存閾值之間需要留有一部分余量。
對(duì)于文章開始所述問題,使用 SetMaxHeap 后,預(yù)期的 GC 過程大概是這個(gè)樣子
簡單用法1
該方法簡單粗暴,直接將 goal 設(shè)置為了固定值
注:通過上文所講,觸發(fā) GC 實(shí)際上是 gc_trigger ,所以當(dāng)閾值設(shè)置為 12GB 時(shí),會(huì)提前一點(diǎn)觸發(fā) GC ,這里為了描述方便,近似認(rèn)為 gc_trigger=goal
簡單用法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 閾值
注:通過上文所講,觸發(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):
下面源碼中的官方注釋說的比較清楚,在一些關(guān)鍵位置加入了中文注釋
入?yún)ytes為要設(shè)置的閾值
notify 簡單理解為 GC 的策略 發(fā)生變化時(shí)會(huì)向 channel 發(fā)送通知,后續(xù)源碼可以看出“策略”具體指哪些內(nèi)容
返回值為本次設(shè)置之前的 MaxHeap 值
$GOROOT/src/runtime/debug/garbage.go
$GOROOT/src/runtime/mgc.go
注:作者盡量用通俗易懂的語言去解釋 Go 的一些機(jī)制和 SetMaxHeap 功能,可能有些描述與實(shí)現(xiàn)細(xì)節(jié)不完全一致,如有錯(cuò)誤還請(qǐng)指出
Go語言——goroutine并發(fā)模型
參考:
Goroutine并發(fā)調(diào)度模型深度解析手?jǐn)]一個(gè)協(xié)程池
Golang 的 goroutine 是如何實(shí)現(xiàn)的?
Golang - 調(diào)度剖析【第二部分】
OS線程初始棧為2MB。Go語言中,每個(gè)goroutine采用動(dòng)態(tài)擴(kuò)容方式,初始2KB,按需增長,最大1G。此外GC會(huì)收縮棧空間。
BTW,增長擴(kuò)容都是有代價(jià)的,需要copy數(shù)據(jù)到新的stack,所以初始2KB可能有些性能問題。
更多關(guān)于stack的內(nèi)容,可以參見大佬的文章。 聊一聊goroutine stack
用戶線程的調(diào)度以及生命周期管理都是用戶層面,Go語言自己實(shí)現(xiàn)的,不借助OS系統(tǒng)調(diào)用,減少系統(tǒng)資源消耗。
Go語言采用兩級(jí)線程模型,即用戶線程與內(nèi)核線程KSE(kernel scheduling entity)是M:N的。最終goroutine還是會(huì)交給OS線程執(zhí)行,但是需要一個(gè)中介,提供上下文。這就是G-M-P模型
Go調(diào)度器有兩個(gè)不同的運(yùn)行隊(duì)列:
go1.10\src\runtime\runtime2.go
Go調(diào)度器根據(jù)事件進(jìn)行上下文切換。
調(diào)度的目的就是防止M堵塞,空閑,系統(tǒng)進(jìn)程切換。
詳見 Golang - 調(diào)度剖析【第二部分】
Linux可以通過epoll實(shí)現(xiàn)網(wǎng)絡(luò)調(diào)用,統(tǒng)稱網(wǎng)絡(luò)輪詢器N(Net Poller)。
文件IO操作
上面都是防止M堵塞,任務(wù)竊取是防止M空閑
每個(gè)M都有一個(gè)特殊的G,g0。用于執(zhí)行調(diào)度,gc,棧管理等任務(wù),所以g0的棧稱為調(diào)度棧。g0的棧不會(huì)自動(dòng)增長,不會(huì)被gc,來自os線程的棧。
go1.10\src\runtime\proc.go
G沒辦法自己運(yùn)行,必須通過M運(yùn)行
M通過通過調(diào)度,執(zhí)行G
從M掛載P的runq中找到G,執(zhí)行G
Golang什么時(shí)候會(huì)觸發(fā)GC
Golang采用了三色標(biāo)記法來進(jìn)行垃圾回收,那么在什么場(chǎng)景下會(huì)觸發(fā)這個(gè)回收動(dòng)作呢?
源碼主要位于文件 src/runtime/mgc.go go version 1.16
觸發(fā)條件從大方面說,可分為 手動(dòng)觸發(fā) 和 系統(tǒng)觸發(fā) 兩種方式。手動(dòng)觸發(fā)一般很少用,主要由開發(fā)者通過調(diào)用 runtime.GC() 函數(shù)來實(shí)現(xiàn),而對(duì)于系統(tǒng)自動(dòng)觸發(fā)是 運(yùn)行時(shí) 根據(jù)一些條件判斷來進(jìn)行的,這也正是本文要介紹的內(nèi)容。
不管哪種觸發(fā)方式,底層回收機(jī)制是一樣的,所以我們先看一下手動(dòng)觸發(fā),根據(jù)它來找系統(tǒng)觸發(fā)的條件。
可以看到開始執(zhí)行GC的是 gcStart() 函數(shù),它有一個(gè) gcTrigger 參數(shù),是一個(gè)觸發(fā)條件結(jié)構(gòu)體,它的結(jié)構(gòu)體也很簡單。
其實(shí)在Golang 內(nèi)部所有的GC都是通過 gcStart() 函數(shù),然后指定一個(gè) gcTrigger 的參數(shù)來開始的,而手動(dòng)觸發(fā)指定的條件值為 gcTriggerCycle 。 gcStart 是一個(gè)很復(fù)雜的函數(shù),有興趣的可以看一下源碼實(shí)現(xiàn)。
對(duì)于 kind 的值有三種,分別為 gcTriggerHeap 、 gcTriggerTime 和 gcTriggerCycle 。
運(yùn)行時(shí)會(huì)通過 gcTrigger.test() 函數(shù)來決定是否需要觸發(fā)GC,只要滿足上面基中一個(gè)即可。
到此我們基本明白了這三種觸發(fā)GC的條件,那么對(duì)于系統(tǒng)自動(dòng)觸發(fā)這種,Golang 從一個(gè)程序的開始到運(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ì)通過 goparkunlock() 函數(shù)主動(dòng)讓自己陷入休眠,以后由 sysmon() 監(jiān)控線程根據(jù)條件來恢復(fù)這個(gè)gc goroutine。
可以看到 sysmon() 會(huì)在一個(gè) for 語句里一直判斷這個(gè) gcTriggerTime 這個(gè)條件是否滿足,如果滿足的話,會(huì)將 forcegc.g 這個(gè) goroutine 添加到全局隊(duì)列里進(jìn)行調(diào)度(這里 forcegc 是一個(gè)全局變量)。
調(diào)度器在調(diào)度循環(huán) runtime.schedule 中還可以通過垃圾收集控制器的 runtime.gcControllerState.findRunnabledGCWorker 獲取并執(zhí)行用于后臺(tái)標(biāo)記的任務(wù)。
當(dāng)前名稱:go語言如何做gc,go 語言 教程
文章來源:http://www.ef60e0e.cn/article/hsggod.html