Golang實(shí)驗(yàn)性功能SetMaxHeap 固定值GC

簡(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)景

setmaxheap-1.png

為什么要降低GC頻率?

GC會(huì)STWStop The World),對(duì)于時(shí)延敏感場(chǎng)景,在一個(gè)周期內(nèi)連續(xù)觸發(fā)兩輪GC,那么STWGC占用的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ō)明

setmaxheap2.png

對(duì)應(yīng)關(guān)系如下:

  • GC開始時(shí)內(nèi)存使用量:GC trigger

  • GC標(biāo)記完成時(shí)內(nèi)存使用量:Heap size at GC completion

  • GC標(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大約為goal90%左右),每輪GC標(biāo)記完成時(shí),會(huì)根據(jù)|Ha-Hg|和實(shí)際使用的cpu資源 動(dòng)態(tài)調(diào)整gc_triggergoal的差值

goalgc_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左右

setmaxheap-3.png

前面一輪看起來(lái)很好,提升了GC觸發(fā)的閾值到10GB,但是如果某一輪GC后的存活對(duì)象到達(dá)2.5GB的時(shí)候,那么下一輪GC觸發(fā)的閾值,將會(huì)超過(guò)內(nèi)存閾值,造成OOMOut 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)致什么呢?

由于goalgc_trigger被設(shè)置成了極大值,mark assistsweep assist也會(huì)按照這個(gè)錯(cuò)誤的值去計(jì)算,導(dǎo)致工作量預(yù)估錯(cuò)誤,這一點(diǎn)可以從trace中進(jìn)行證明

setmaxheap4.png

setmaxheap5.png

可以看到很詭異的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è)置為了固定值

setmaxheap-6.png

注:通過(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閾值

setmaxheap-7.png

注:通過(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)注明出處,謝謝

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容