簡(jiǎn)單來(lái)說(shuō),SetMaxHeap提供了一種可以設(shè)置固定觸發(fā)閾值的 GC(Garbage Collection垃圾回收)方式
官方源碼鏈接 https://go-review.googlesource.com/c/go/+/227767/3
可以解決什么問(wèn)題?
大量臨時(shí)對(duì)象分配導(dǎo)致的GC觸發(fā)頻率過(guò)高,GC后實(shí)際存活的對(duì)象較少,
或者機(jī)器內(nèi)存較充足,希望使用剩余內(nèi)存,降低GC頻率的場(chǎng)景

為什么要降低GC頻率?
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)景的解決方案
這里簡(jiǎn)單說(shuō)下GC的幾個(gè)值的含義,可通過(guò)GODEBUG=gctrace=1獲得如下數(shù)據(jù)
gc 16 @1.106s 3%: 0.010+19+0.038 ms clock, 0.21+0.29/95/266+0.76 ms cpu, 128->132->67 MB, 135 MB goal, 20 P
gc 17 @1.236s 3%: 0.010+20+0.040 ms clock, 0.21+0.37/100/267+0.81 ms cpu, 129->132->67 MB, 135 MB goal, 20 P
這里只關(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ì)文檔中的一張圖來(lái)說(shuō)明

對(duì)應(yīng)關(guān)系如下:
GC開始時(shí)內(nèi)存使用量:
GC triggerGC標(biāo)記完成時(shí)內(nèi)存使用量:
Heap size at GC completionGC標(biāo)記完成時(shí)的存活內(nèi)存量:圖中標(biāo)記的
Previous marked heap size為上一輪的GC標(biāo)記完成時(shí)的存活內(nèi)存量本輪GC標(biāo)記完成時(shí)的預(yù)期內(nèi)存使用量:
Goal heap size
簡(jiǎn)單說(shuō)下GC pacing(信用機(jī)制)
GC pacing有兩個(gè)目標(biāo),
|Ha-Hg|最小GC使用的cpu資源約為總數(shù)的25%
那么當(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所需的工作量的方式如下:
// heap_scan is the number of bytes of "scannable" heap. This
// is the live heap (as counted by heap_live), but omitting
// no-scan objects and no-scan tails of objects.
//
// Whenever this is updated, call gcController.revise().
heap_scan uint64
// Compute the expected scan work remaining.
//
// This is estimated based on the expected
// steady-state scannable heap. For example, with
// GOGC=100, only half of the scannable heap is
// expected to be live, so that's what we target.
//
// (This is a float calculation to avoid overflowing on
// 100*heap_scan.)
scanWorkExpected := int64(float64(memstats.heap_scan) * 100 / float64(100+gcpercent))
如何解決問(wèn)題
繼續(xù)分析文章開頭的問(wèn)題,如何充分利用剩余內(nèi)存,降低GC頻率和GC對(duì)CPU的資源消耗
增大 gcpercent?
如上圖可以看出,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ān)閉GC,監(jiān)控堆內(nèi)存使用狀態(tài),手動(dòng)進(jìn)行GC?
可以通過(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)也沒什么問(wèn)題,將閾值調(diào)大,預(yù)期永遠(yuǎn)不會(huì)再觸發(fā)GC
^uint64(0)>>20 == 17592186044415 MB
那么如果在關(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
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)存閾值之間需要留有一部分余量。
stacks_sys uint64 // only counts newosproc0 stack in mstats; differs from MemStats.StackSys
mspan_sys uint64
mcache_sys uint64
buckhash_sys uint64 // profiling bucket hash table
gc_sys uint64 // updated atomically or during STW
other_sys uint64 // updated atomically or during STW
對(duì)于文章開始所述問(wèn)題,使用SetMaxHeap后,預(yù)期的GC過(guò)程大概是這個(gè)樣子
簡(jiǎn)單用法1
notify := make(chan struct{}, 1) // 先不管這個(gè)notify
debug.SetMaxHeap(12<<30, notify) // 設(shè)置閾值為12GB
debug.SetGCPercent(-1) // 該方案實(shí)現(xiàn)過(guò)程中對(duì)關(guān)閉GC的情況做了兼容,可以這樣使用
該方法簡(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
notify := make(chan struct{}, 1) // 先不管這個(gè)notify
debug.SetMaxHeap(12<<30, notify) // 設(shè)置閾值為12GB
當(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 "https://go.googlesource.com/go" refs/changes/67/227767/3 && git cherry-pick FETCH_HEAD
需要重新編譯Go源碼
實(shí)現(xiàn)原理
注意點(diǎn):
- 是對(duì)于
Go堆內(nèi)存的軟限制 - 設(shè)置
debug.SetMaxHeap后,即使關(guān)閉GC,sysmon監(jiān)控到超過(guò)2分鐘沒有觸發(fā)GC時(shí),仍會(huì)觸發(fā)GC
下面源碼中的官方注釋說(shuō)的比較清楚,在一些關(guān)鍵位置加入了中文注釋
函數(shù)原型
func SetMaxHeap(bytes uintptr, notify chan<- struct{}) uintptr
入?yún)ytes為要設(shè)置的閾值
notify 簡(jiǎn)單理解為 GC的策略 發(fā)生變化時(shí)會(huì)向channel發(fā)送通知,后續(xù)源碼可以看出“策略”具體指哪些內(nèi)容
Whenever the garbage collector's scheduling policy changes as a result of this heap limit (that is, the result that would be returned by ReadGCPolicy changes), the garbage collector will send to the notify channel.
返回值為本次設(shè)置之前的MaxHeap值
官方推薦用法
notify := make(chan struct{}, 1)
var gcp GCPolicy
prev := SetMaxHeap(limit, notify)
// Check that that notified us of heap pressure.
select {
case <-notify:
ReadGCPolicy(&gcp) // 獲取變化后的GC策略信息
default:
t.Errorf("missing GC pressure notification")
}
$GOROOT/src/runtime/debug/garbage.go
// SetGCPercent sets the garbage collection target percentage:
// a collection is triggered when the ratio of freshly allocated data
// to live data remaining after the previous collection reaches this percentage.
// SetGCPercent returns the previous setting.
// The initial setting is the value of the GOGC environment variable
// at startup, or 100 if the variable is not set.
// A negative percentage disables triggering garbage collection
// based on the ratio of fresh allocation to previously live heap.
// However, GC can still be explicitly triggered by runtime.GC and
// similar functions, or by the maximum heap size set by SetMaxHeap.
func SetGCPercent(percent int) int {
return int(setGCPercent(int32(percent)))
}
// GCPolicy reports the garbage collector's policy for controlling the
// heap size and scheduling garbage collection work.
type GCPolicy struct {
// GCPercent is the current value of GOGC, as set by the GOGC
// environment variable or SetGCPercent.
//
// If triggering GC by relative heap growth is disabled, this
// will be -1.
GCPercent int
// MaxHeapBytes is the current soft heap limit set by
// SetMaxHeap, in bytes.
//
// If there is no heap limit set, this will be ^uintptr(0).
MaxHeapBytes uintptr
// AvailGCPercent is the heap space available for allocation
// before the next GC, as a percent of the heap used at the
// end of the previous garbage collection. It measures memory
// pressure and how hard the garbage collector must work to
// achieve the heap size goals set by GCPercent and
// MaxHeapBytes.
//
// For example, if AvailGCPercent is 100, then at the end of
// the previous garbage collection, the space available for
// allocation before the next GC was the same as the space
// used. If AvailGCPercent is 20, then the space available is
// only a 20% of the space used.
//
// AvailGCPercent is directly comparable with GCPercent.
//
// If AvailGCPercent >= GCPercent, the garbage collector is
// not under pressure and can amortize the cost of garbage
// collection by allowing the heap to grow in proportion to
// how much is used.
//
// If AvailGCPercent < GCPercent, the garbage collector is
// under pressure and must run more frequently to keep the
// heap size under MaxHeapBytes. Smaller values of
// AvailGCPercent indicate greater pressure. In this case, the
// application should shed load and reduce its live heap size
// to relieve memory pressure.
//
// AvailGCPercent is always >= 0.
AvailGCPercent int
}
// 只是針對(duì)Go堆內(nèi)存的軟限制,默認(rèn)不開啟
// SetMaxHeap sets a soft limit on the size of the Go heap and returns
// the previous setting. By default, there is no limit.
//
// If a max heap is set, the garbage collector will endeavor to keep
// the heap size under the specified size, even if this is lower than
// would normally be determined by GOGC (see SetGCPercent).
//
// 當(dāng)由于SetMaxHeap導(dǎo)致垃圾收集器的策略發(fā)生變化(ReadGCPolicy返回的結(jié)果發(fā)生更改)時(shí),
// 會(huì)向notify發(fā)生通知
// Whenever the garbage collector's scheduling policy changes as a
// result of this heap limit (that is, the result that would be
// returned by ReadGCPolicy changes), the garbage collector will send
// to the notify channel. This is a non-blocking send, so this should
// be a single-element buffered channel, though this is not required.
// Only a single channel may be registered for notifications at a
// time; SetMaxHeap replaces any previously registered channel.
//
// The application is strongly encouraged to respond to this
// notification by calling ReadGCPolicy and, if AvailGCPercent is less
// than GCPercent, shedding load to reduce its live heap size. Setting
// a maximum heap size limits the garbage collector's ability to
// amortize the cost of garbage collection when the heap reaches the
// heap size limit. This is particularly important in
// request-processing systems, where increasing pressure on the
// garbage collector reduces CPU time available to the application,
// making it less able to complete work, leading to even more pressure
// on the garbage collector. The application must shed load to avoid
// this "GC death spiral".
//
// The limit set by SetMaxHeap is soft. If the garbage collector would
// consume too much CPU to keep the heap under this limit (leading to
// "thrashing"), it will allow the heap to grow larger than the
// specified max heap.
//
// 堆內(nèi)存大小不是包括進(jìn)程內(nèi)存占用的所有內(nèi)容,它不包括stacks,C分配的內(nèi)存和許多runtime內(nèi)部使用的結(jié)構(gòu)。
// The heap size does not include everything in the process's memory
// footprint. Notably, it does not include stacks, C-allocated memory,
// or many runtime-internal structures.
//
// 當(dāng)bytes== ^uintptr(0)時(shí),關(guān)閉SetMaxHeap,僅當(dāng)這種情況下notify可以為nil
// To disable the heap limit, pass ^uintptr(0) for the bytes argument.
// In this case, notify can be nil.
//
// 如果只依賴SetMaxHeap去觸發(fā)GC,在設(shè)置閾值后調(diào)用SetGCPercent(-1)來(lái)關(guān)閉原生GC
// To depend only on the heap limit to trigger garbage collection,
// call SetGCPercent(-1) after setting a heap limit.
func SetMaxHeap(bytes uintptr, notify chan<- struct{}) uintptr {
// 關(guān)閉該功能
if bytes == ^uintptr(0) {
return gcSetMaxHeap(bytes, nil)
}
// 開啟該功能時(shí),應(yīng)傳入一個(gè)notify
if notify == nil {
panic("SetMaxHeap requires a non-nil notify channel")
}
return gcSetMaxHeap(bytes, notify)
}
// gcSetMaxHeap is provided by package runtime.
// 在這里只進(jìn)行函數(shù)定義,在runtime中實(shí)現(xiàn)具體功能
func gcSetMaxHeap(bytes uintptr, notify chan<- struct{}) uintptr
// ReadGCPolicy reads the garbage collector's current policy for
// managing the heap size. This includes static settings controlled by
// the application and dynamic policy determined by heap usage.
// ReadGCPolicy讀取垃圾收集器當(dāng)前的用于管理堆大小的策略,
// 這包括由應(yīng)用程序控制的靜態(tài)設(shè)置和由堆使用情況確定的動(dòng)態(tài)策略,后面可以看下具體返回的是什么
func ReadGCPolicy(gcp *GCPolicy) {
gcp.GCPercent, gcp.MaxHeapBytes, gcp.AvailGCPercent = gcReadPolicy()
}
// gcReadPolicy is provided by package runtime.
// 在這里只進(jìn)行函數(shù)定義,在runtime中實(shí)現(xiàn)具體功能
func gcReadPolicy() (gogc int, maxHeap uintptr, egogc int)
$GOROOT/src/runtime/mgc.go
// To begin with, maxHeap is infinity.
// 默認(rèn)關(guān)閉,為極大值
var maxHeap uintptr = ^uintptr(0)
func gcinit() {
if unsafe.Sizeof(workbuf{}) != _WorkbufSize {
throw("size of Workbuf is suboptimal")
}
// No sweep on the first cycle.
mheap_.sweepdone = 1
// Set a reasonable initial GC trigger.
memstats.triggerRatio = 7 / 8.0
// Fake a heap_marked value so it looks like a trigger at
// heapminimum is the appropriate growth from heap_marked.
// This will go into computing the initial GC goal.
memstats.heap_marked = uint64(float64(heapminimum) / (1 + memstats.triggerRatio))
// Disable heap limit initially.
// 默認(rèn)關(guān)閉
gcPressure.maxHeap = ^uintptr(0)
// Set gcpercent from the environment. This will also compute
// and set the GC trigger and goal.
_ = setGCPercent(readgogc())
work.startSema = 1
work.markDoneSema = 1
}
//go:linkname setGCPercent runtime/debug.setGCPercent
func setGCPercent(in int32) (out int32) {
// Run on the system stack since we grab the heap lock.
var updated bool
systemstack(func() {
lock(&mheap_.lock)
out = gcpercent
if in < 0 {
in = -1
}
gcpercent = in
// 為了做兼容,gcpercent<0時(shí),不再將goal設(shè)置為極大值
if gcpercent >= 0 {
heapminimum = defaultHeapMinimum * uint64(gcpercent) / 100
} else {
heapminimum = 0
}
// Update pacing in response to gcpercent change.
updated = gcSetTriggerRatio(memstats.triggerRatio)
unlock(&mheap_.lock)
})
// Pacing changed, so the scavenger should be awoken.
wakeScavenger()
// 如果開啟了SetMaxHeap,并且發(fā)生變化時(shí),則向notify發(fā)送通知
if updated {
gcPolicyNotify()
}
// If we just disabled GC, wait for any concurrent GC mark to
// finish so we always return with no GC running.
if in < 0 {
gcWaitOnMark(atomic.Load(&work.cycles))
}
return out
}
// 新增SetMaxHeap相關(guān)內(nèi)部變量
var gcPressure struct {
// lock may be acquired while mheap_.lock is held. Hence, it
// must only be acquired from the system stack.
lock mutex
// notify is a notification channel for GC pressure changes
// with a notification sent after every gcSetTriggerRatio.
// It is provided by package debug. It may be nil.
notify chan<- struct{}
// Together gogc, maxHeap, and egogc represent the GC policy.
//
// gogc is GOGC, maxHeap is the GC heap limit, and egogc is the effective GOGC.
// gogc,maxHeap和egogc一起代表了GC策略
// gogc是GOGC,maxHeap是GC堆限制,egogc是有效的GOGC
//
// These are set by the user with debug.SetMaxHeap. GC will
// attempt to keep heap_live under maxHeap, even if it has to
// violate GOGC (up to a point).
gogc int
maxHeap uintptr
egogc int
}
//go:linkname gcSetMaxHeap runtime/debug.gcSetMaxHeap
// 通過(guò)linkname的方式,在這里實(shí)現(xiàn)debug包中的gcSetMaxHeap函數(shù)內(nèi)容
func gcSetMaxHeap(bytes uintptr, notify chan<- struct{}) uintptr {
var (
prev uintptr
updated bool
)
systemstack(func() {
// gcPressure.notify has a write barrier on it so it must be protected
// by gcPressure's lock instead of mheap's, otherwise we could deadlock.
// 加鎖,獲取設(shè)置之前的閾值,將bytes設(shè)置到maxHeap中
lock(&gcPressure.lock)
gcPressure.notify = notify
unlock(&gcPressure.lock)
lock(&mheap_.lock)
// Update max heap.
prev = maxHeap
maxHeap = bytes
// Update pacing. This will update gcPressure from the
// globals gcpercent and maxHeap.
updated = gcSetTriggerRatio(memstats.triggerRatio)
unlock(&mheap_.lock)
})
if updated {
gcPolicyNotify()
}
// 返回設(shè)置之前的閾值
return prev
}
// gcSetTriggerRatio sets the trigger ratio and updates everything
// derived from it: the absolute trigger, the heap goal, mark pacing,
// and sweep pacing.
//
// This can be called any time. If GC is the in the middle of a
// concurrent phase, it will adjust the pacing of that phase.
//
// This depends on gcpercent, mheap_.maxHeap, memstats.heap_marked,
// and memstats.heap_live. These must be up to date.
//
// Returns whether or not there was a change in the GC policy.
// If it returns true, the caller must call gcPolicyNotify() after
// releasing the heap lock.
//
// mheap_.lock must be held or the world must be stopped.
//
// This must be called on the system stack because it acquires
// gcPressure.lock.
//
//go:systemstack
// 新增返回值changed,當(dāng)返回true時(shí)會(huì)向notify發(fā)送通知
// 該函數(shù)會(huì)在gcSetMaxHeap、setGCPercent、gcMarkTermination中調(diào)用
// gcSetMaxHeap、setGCPercent只會(huì)被主動(dòng)調(diào)用;gcMarkTermination在每輪GC的STW2期間被調(diào)用
func gcSetTriggerRatio(triggerRatio float64) (changed bool) {
// Since GOGC ratios are in terms of heap_marked, make sure it
// isn't 0. This shouldn't happen, but if it does we want to
// avoid infinities and divide-by-zeroes.
if memstats.heap_marked == 0 {
memstats.heap_marked = 1
}
// Compute the next GC goal, which is when the allocated heap
// has grown by GOGC/100 over the heap marked by the last
// cycle, or maxHeap, whichever is lower.
// 先根據(jù)gcpercent計(jì)算goal值
goal := ^uint64(0)
if gcpercent >= 0 {
goal = memstats.heap_marked + memstats.heap_marked*uint64(gcpercent)/100
}
lock(&gcPressure.lock)
// 如果開啟了SetMaxHeap功能,并且goal大于設(shè)置的閾值,強(qiáng)行修改goal
if gcPressure.maxHeap != ^uintptr(0) && goal > uint64(gcPressure.maxHeap) { // Careful of 32-bit uintptr!
// Use maxHeap-based goal.
goal = uint64(gcPressure.maxHeap)
unlock(&gcPressure.lock)
// Avoid thrashing by not letting the
// effective GOGC drop below 10.
//
// TODO(austin): This heuristic is pulled from
// thin air. It might be better to do
// something to more directly force
// amortization of GC costs, e.g., by limiting
// what fraction of the time GC can be active.
var minGOGC uint64 = 10
if gcpercent >= 0 && uint64(gcpercent) < minGOGC {
// The user explicitly requested
// GOGC < minGOGC. Use that.
minGOGC = uint64(gcpercent)
}
lowerBound := memstats.heap_marked + memstats.heap_marked*minGOGC/100
if goal < lowerBound {
goal = lowerBound
}
} else {
unlock(&gcPressure.lock)
}
// Set the trigger ratio, capped to reasonable bounds.
if triggerRatio < 0 {
// This can happen if the mutator is allocating very
// quickly or the GC is scanning very slowly.
triggerRatio = 0
} else if gcpercent >= 0 && triggerRatio > float64(gcpercent)/100 {
// Cap trigger ratio at GOGC/100.
triggerRatio = float64(gcpercent) / 100
}
memstats.triggerRatio = triggerRatio
// Compute the absolute GC trigger from the trigger ratio.
//
// We trigger the next GC cycle when the allocated heap has
// grown by the trigger ratio over the marked heap size.
trigger := ^uint64(0)
// 計(jì)算gc_trigger
if goal != ^uint64(0) {
trigger = uint64(float64(memstats.heap_marked) * (1 + triggerRatio))
// Ensure there's always a little margin so that the
// mutator assist ratio isn't infinity.
if trigger > goal*95/100 {
trigger = goal * 95 / 100
}
// If we let triggerRatio go too low, then if the application
// is allocating very rapidly we might end up in a situation
// where we're allocating black during a nearly always-on GC.
// The result of this is a growing heap and ultimately an
// increase in RSS. By capping us at a point >0, we're essentially
// saying that we're OK using more CPU during the GC to prevent
// this growth in RSS.
//
// The current constant was chosen empirically: given a sufficiently
// fast/scalable allocator with 48 Ps that could drive the trigger ratio
// to <0.05, this constant causes applications to retain the same peak
// RSS compared to not having this allocator.
const minTriggerRatio = 0.6
minTrigger := memstats.heap_marked + uint64(minTriggerRatio*float64(goal-memstats.heap_marked))
if trigger < minTrigger {
trigger = minTrigger
}
// Don't trigger below the minimum heap size.
minTrigger = heapminimum
if !isSweepDone() {
// Concurrent sweep happens in the heap growth
// from heap_live to gc_trigger, so ensure
// that concurrent sweep has some heap growth
// in which to perform sweeping before we
// start the next GC cycle.
sweepMin := atomic.Load64(&memstats.heap_live) + sweepMinHeapDistance
if sweepMin > minTrigger {
minTrigger = sweepMin
}
}
if trigger < minTrigger {
trigger = minTrigger
}
if int64(trigger) < 0 {
print("runtime: next_gc=", memstats.next_gc, " heap_marked=", memstats.heap_marked, " heap_live=", memstats.heap_live, " initialHeapLive=", work.initialHeapLive, "triggerRatio=", triggerRatio, " minTrigger=", minTrigger, "\n")
throw("gc_trigger underflow")
}
if trigger > goal {
// The trigger ratio is always less than GOGC/100, but
// other bounds on the trigger may have raised it.
// Push up the goal, too.
goal = trigger
}
}
// Commit to the trigger and goal.
memstats.gc_trigger = trigger
memstats.next_gc = goal
if trace.enabled {
traceNextGC()
}
// Update mark pacing.
if gcphase != _GCoff {
gcController.revise()
}
// Update sweep pacing.
if isSweepDone() {
mheap_.sweepPagesPerByte = 0
} else {
// Concurrent sweep needs to sweep all of the in-use
// pages by the time the allocated heap reaches the GC
// trigger. Compute the ratio of in-use pages to sweep
// per byte allocated, accounting for the fact that
// some might already be swept.
heapLiveBasis := atomic.Load64(&memstats.heap_live)
heapDistance := int64(trigger) - int64(heapLiveBasis)
// Add a little margin so rounding errors and
// concurrent sweep are less likely to leave pages
// unswept when GC starts.
heapDistance -= 1024 * 1024
if heapDistance < _PageSize {
// Avoid setting the sweep ratio extremely high
heapDistance = _PageSize
}
pagesSwept := atomic.Load64(&mheap_.pagesSwept)
pagesInUse := atomic.Load64(&mheap_.pagesInUse)
sweepDistancePages := int64(pagesInUse) - int64(pagesSwept)
if sweepDistancePages <= 0 {
mheap_.sweepPagesPerByte = 0
} else {
mheap_.sweepPagesPerByte = float64(sweepDistancePages) / float64(heapDistance)
mheap_.sweepHeapLiveBasis = heapLiveBasis
// Write pagesSweptBasis last, since this
// signals concurrent sweeps to recompute
// their debt.
atomic.Store64(&mheap_.pagesSweptBasis, pagesSwept)
}
}
gcPaceScavenger()
// Update the GC policy due to a GC pressure change.
lock(&gcPressure.lock)
gogc, maxHeap, egogc := gcReadPolicyLocked()
// 如果gogc、maxHeap、egogc中的任何一個(gè)值與上次調(diào)用gcSetTriggerRatio時(shí)的值不一樣,則返回true
if gogc != gcPressure.gogc || maxHeap != gcPressure.maxHeap || egogc != gcPressure.egogc {
gcPressure.gogc, gcPressure.maxHeap, gcPressure.egogc = gogc, maxHeap, egogc
changed = true
}
unlock(&gcPressure.lock)
return
}
// Sends a non-blocking notification on gcPressure.notify.
//
// mheap_.lock and gcPressure.lock must not be held.
// 簡(jiǎn)單做些判斷后,向notify以非阻塞的方式發(fā)送通知
func gcPolicyNotify() {
// Switch to the system stack to acquire gcPressure.lock.
var n chan<- struct{}
gp := getg()
systemstack(func() {
lock(&gcPressure.lock)
if gcPressure.notify == nil {
unlock(&gcPressure.lock)
return
}
if raceenabled {
// notify is protected by gcPressure.lock, but
// the race detector can't see that.
raceacquireg(gp, unsafe.Pointer(&gcPressure.notify))
}
// Just grab the channel first so that we're holding as
// few locks as possible when we actually make the channel send.
n = gcPressure.notify
if raceenabled {
racereleaseg(gp, unsafe.Pointer(&gcPressure.notify))
}
unlock(&gcPressure.lock)
})
if n == nil {
return
}
// Perform a non-blocking send on the channel.
select {
case n <- struct{}{}:
default:
}
}
//go:linkname gcReadPolicy runtime/debug.gcReadPolicy
func gcReadPolicy() (gogc int, maxHeap uintptr, egogc int) {
systemstack(func() {
lock(&mheap_.lock)
gogc, maxHeap, egogc = gcReadPolicyLocked()
unlock(&mheap_.lock)
})
return
}
// mheap_.lock must be locked, therefore this must be called on the
// systemstack.
//go:systemstack
// 按照前面的注釋所述,gogc是GOGC,maxHeap是GC堆限制,egogc是有效的GOGC
func gcReadPolicyLocked() (gogc int, maxHeapOut uintptr, egogc int) {
// 獲取計(jì)算出的實(shí)際goal值
goal := memstats.next_gc
// 如果goal小于閾值,并且沒有關(guān)閉GC,那么是按照正常的gcpercent計(jì)算的,沒有進(jìn)行干預(yù)
// 參考簡(jiǎn)單用法2,時(shí)間點(diǎn)5時(shí)的GC觸發(fā)值,返回gcpercent
if goal < uint64(maxHeap) && gcpercent >= 0 {
// We're not up against the max heap size, so just
// return GOGC.
egogc = int(gcpercent)
} else {
// Back out the effective GOGC from the goal.
// 獲取實(shí)際情況的gcpercent
egogc = int(gcEffectiveGrowthRatio() * 100)
// The effective GOGC may actually be higher than
// gcpercent if the heap is tiny. Avoid that confusion
// and just return the user-set GOGC.
// 當(dāng)開啟GC,并且實(shí)際GOGC大于設(shè)置的gcpercent時(shí),仍然返回設(shè)置的gcpercent
if gcpercent >= 0 && egogc > int(gcpercent) {
egogc = int(gcpercent)
}
}
// 返回的三個(gè)值分別為 設(shè)置的gcpercent、設(shè)置的maxHeap、當(dāng)前實(shí)際的gcpercent
return int(gcpercent), maxHeap, egogc
}
func gcEffectiveGrowthRatio() float64 {
// 計(jì)算實(shí)際的 gcpercent,根據(jù)本輪GC存活對(duì)象和下一輪GC的goal
// `goal = memstats.heap_marked + memstats.heap_marked*uint64(gcpercent)/100` 這個(gè)公式的逆運(yùn)算
egogc := float64(memstats.next_gc-memstats.heap_marked) / float64(memstats.heap_marked)
if egogc < 0 {
// Shouldn't happen, but just in case.
egogc = 0
}
return egogc
}
// test reports whether the trigger condition is satisfied, meaning
// that the exit condition for the _GCoff phase has been met. The exit
// condition should be tested when allocating.
// 這個(gè)函數(shù)是計(jì)算是否需要觸發(fā)GC的,返回true時(shí)會(huì)調(diào)用gcStart 進(jìn)行GC
func (t gcTrigger) test() bool {
if !memstats.enablegc || panicking != 0 || gcphase != _GCoff {
return false
}
switch t.kind {
case gcTriggerHeap:
// Non-atomic access to heap_live for performance. If
// we are going to trigger on this, this thread just
// atomically wrote heap_live anyway and we'll see our
// own write.
return memstats.heap_live >= memstats.gc_trigger
case gcTriggerTime:
if gcpercent < 0 && gcPressure.maxHeap == ^uintptr(0) {
return false
}
// 如果設(shè)置了debug.SetMaxHeap,即使配置debug.SetGCPercent(-1)時(shí),超過(guò)2分鐘沒有觸發(fā)GC時(shí),仍然觸發(fā)GC
lastgc := int64(atomic.Load64(&memstats.last_gc_nanotime))
return lastgc != 0 && t.now-lastgc > forcegcperiod
case gcTriggerCycle:
// t.n > work.cycles, but accounting for wraparound.
return int32(t.n-work.cycles) > 0
}
return true
}
func gcMarkTermination(nextTriggerRatio float64) {
……
……
// Update GC trigger and pacing for the next cycle.
var notify bool
systemstack(func() {
notify = gcSetTriggerRatio(nextTriggerRatio)
})
if notify {
gcPolicyNotify()
}
……
……
}
注:作者盡量用通俗易懂的語(yǔ)言去解釋Go的一些機(jī)制和SetMaxHeap功能,可能有些描述與實(shí)現(xiàn)細(xì)節(jié)不完全一致,如有錯(cuò)誤還請(qǐng)指出
轉(zhuǎn)載請(qǐng)注明出處,謝謝