轉(zhuǎn)載于:這里
Golang 從第一個版本以來,GC 一直是大家詬病最多的尼斧。但是每一個版本的發(fā)布基本都伴隨著 GC 的改進(jìn)东涡。下面列出一些比較重要的改動。
v1.1 STW
v1.3 Mark STW, Sweep 并行
v1.5 三色標(biāo)記法
v1.8 hybrid write barrier
GC 算法簡介
這一小節(jié)介紹三種經(jīng)典的 GC 算法:引用計數(shù)(reference counting)界阁、標(biāo)記-清掃(mark & sweep)、節(jié)點復(fù)制(Copying Garbage Collection)胖喳,分代收集(Generational Garbage Collection)泡躯。
引用計數(shù)
引用計數(shù)的思想非常簡單:每個單元維護(hù)一個域,保存其它單元指向它的引用數(shù)量(類似有向圖的入度)丽焊。當(dāng)引用數(shù)量為 0 時较剃,將其回收。引用計數(shù)是漸進(jìn)式的技健,能夠?qū)?nèi)存管理的開銷分布到整個程序之中写穴。C++ 的 share_ptr 使用的就是引用計算方法。
引用計數(shù)算法實現(xiàn)一般是把所有的單元放在一個單元池里雌贱,比如類似 free list啊送。這樣所有的單元就被串起來了,就可以進(jìn)行引用計數(shù)了欣孤。新分配的單元計數(shù)值被設(shè)置為 1(注意不是 0馋没,因為申請一般都說 ptr = new object 這種)。每次有一個指針被設(shè)為指向該單元時导街,該單元的計數(shù)值加 1披泪;而每次刪除某個指向它的指針時,它的計數(shù)值減 1搬瑰。當(dāng)其引用計數(shù)為 0 的時候款票,該單元會被進(jìn)行回收控硼。雖然這里說的比較簡單,實現(xiàn)的時候還是有很多細(xì)節(jié)需要考慮艾少,比如刪除某個單元的時候卡乾,那么它指向的所有單元都需要對引用計數(shù)減 1。那么如果這個時候缚够,發(fā)現(xiàn)其中某個指向的單元的引用計數(shù)又為 0幔妨,那么是遞歸的進(jìn)行還是采用其他的策略呢?遞歸處理的話會導(dǎo)致系統(tǒng)顛簸谍椅。關(guān)于這些細(xì)節(jié)這里就不討論了误堡,可以參考文章后面的給的參考資料。
優(yōu)點
- 漸進(jìn)式雏吭。內(nèi)存管理與用戶程序的執(zhí)行交織在一起锁施,將 GC 的代價分散到整個程序。不像標(biāo)記-清掃算法需要 STW (Stop The World杖们,GC 的時候掛起用戶程序)悉抵。
- 算法易于實現(xiàn)。
- 內(nèi)存單元能夠很快被回收摘完。相比于其他垃圾回收算法姥饰,堆被耗盡或者達(dá)到某個閾值才會進(jìn)行垃圾回收。
缺點
原始的引用計數(shù)不能處理循環(huán)引用孝治。大概這是被詬病最多的缺點了列粪。不過針對這個問題,也除了很多解決方案谈飒,比如強(qiáng)引用等篱竭。
維護(hù)引用計數(shù)降低運行效率。內(nèi)存單元的更新刪除等都需要維護(hù)相關(guān)的內(nèi)存單元的引用計數(shù)步绸,相比于一些追蹤式的垃圾回收算法并不需要這些代價。
單元池 free list 實現(xiàn)的話不是 cache-friendly 的吃媒,這樣會導(dǎo)致頻繁的 cache miss瓤介,降低程序運行效率。
標(biāo)記-清掃
標(biāo)記-清掃算法是第一種自動內(nèi)存管理赘那,基于追蹤的垃圾收集算法刑桑。算法思想在 70 年代就提出了,是一種非常古老的算法募舟。內(nèi)存單元并不會在變成垃圾立刻回收祠斧,而是保持不可達(dá)狀態(tài),直到到達(dá)某個閾值或者固定時間長度拱礁。這個時候系統(tǒng)會掛起用戶程序琢锋,也就是 STW辕漂,轉(zhuǎn)而執(zhí)行垃圾回收程序。垃圾回收程序?qū)λ械拇婊顔卧M(jìn)行一次全局遍歷確定哪些單元可以回收吴超。算法分兩個部分:標(biāo)記(mark)和清掃(sweep)钉嘹。標(biāo)記階段表明所有的存活單元,清掃階段將垃圾單元回收鲸阻“匣粒可視化可以參考下圖。
標(biāo)記-清掃算法的優(yōu)點也就是基于追蹤的垃圾回收算法具有的優(yōu)點:避免了引用計數(shù)算法的缺點(不能處理循環(huán)引用鸟悴,需要維護(hù)指針)陈辱。缺點也很明顯,需要 STW细诸。
三色標(biāo)記算法
三色標(biāo)記算法是對標(biāo)記階段的改進(jìn)沛贪,原理如下:
- 起初所有對象都是白色。
- 從根出發(fā)掃描所有可達(dá)對象揍堰,標(biāo)記為灰色鹏浅,放入待處理隊列。
- 從隊列取出灰色對象屏歹,將其引用對象標(biāo)記為灰色放入隊列隐砸,自身標(biāo)記為黑色,并放入黑色集合中蝙眶。
- 重復(fù) 3季希,直到灰色對象隊列為空。此時白色對象即為垃圾幽纷,進(jìn)行回收式塌。
可視化如下:
三色標(biāo)記的一個明顯好處是能夠讓用戶程序和 mark 并發(fā)的進(jìn)行,具體可以參考論文:《On-the-fly garbage collection: an exercise in cooperation.》友浸。Golang 的 GC 實現(xiàn)也是基于這篇論文峰尝,后面再具體說明。
節(jié)點復(fù)制
節(jié)點復(fù)制也是基于追蹤的算法收恢。其將整個堆等分為兩個半?yún)^(qū)(semi-space)武学,一個包含現(xiàn)有數(shù)據(jù),另一個包含已被廢棄的數(shù)據(jù)伦意。節(jié)點復(fù)制是垃圾收集從切換(flip)兩個半?yún)^(qū)的角色開始火窒,然后收集器在老的半?yún)^(qū),也就是 Fromspace 中遍歷存活的數(shù)據(jù)結(jié)構(gòu)驮肉,在第一次訪問某個單元時把它復(fù)制到新半?yún)^(qū)熏矿,也就是 Tospace 中去。在 Fromspace 中所有存活單元都被訪問過之后,收集器在 Tospace 中建立一個存活數(shù)據(jù)結(jié)構(gòu)的副本票编,用戶程序可以重新開始運行了褪储。
優(yōu)點
- 所有存活的數(shù)據(jù)結(jié)構(gòu)都縮并地排列在 Tospace 的底部,這樣就不會存在內(nèi)存碎片的問題栏妖。
- 獲取新內(nèi)存可以簡單地通過遞增自由空間指針來實現(xiàn)乱豆。
缺點
- 內(nèi)存得不到充分利用,總有一半的內(nèi)存空間處于浪費狀態(tài)吊趾。
分代收集
基于追蹤的垃圾回收算法(標(biāo)記-清掃宛裕、節(jié)點復(fù)制)一個主要問題是在生命周期較長的對象上浪費時間(長生命周期的對象是不需要頻繁掃描的)。同時论泛,內(nèi)存分配存在這么一個事實 “most object die young”揩尸∩⌒粒基于這兩點幕帆,分代垃圾回收算法將對象按生命周期長短存放到堆上的兩個(或者更多)區(qū)域,這些區(qū)域就是分代(generation)恭金。對于新生代的區(qū)域的垃圾回收頻率要明顯高于老年代區(qū)域坟瓢。
分配對象的時候從新生代里面分配勇边,如果后面發(fā)現(xiàn)對象的生命周期較長,則將其移到老年代折联,這個過程叫做 promote粒褒。隨著不斷 promote,最后新生代的大小在整個堆的占用比例不會特別大诚镰。收集的時候集中主要精力在新生代就會相對來說效率更高奕坟,STW 時間也會更短。
優(yōu)點:性能更優(yōu)
缺點:實現(xiàn)復(fù)雜
Golang GC
Overview
在說 Golang 的具體垃圾回收流程時清笨,我們先來看一下幾個基本的問題月杉。
何時觸發(fā) GC
在堆上分配大于 32K byte 對象的時候進(jìn)行檢測此時是否滿足垃圾回收條件,如果滿足則進(jìn)行垃圾回收抠艾。
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
...
shouldhelpgc := false
// 分配的對象小于 32K byte
if size <= maxSmallSize {
...
} else {
shouldhelpgc = true
...
}
...
// gcShouldStart() 函數(shù)進(jìn)行觸發(fā)條件檢測
if shouldhelpgc && gcShouldStart(false) {
// gcStart() 函數(shù)進(jìn)行垃圾回收
gcStart(gcBackgroundMode, false)
}
}
上面是自動垃圾回收苛萎,還有一種是主動垃圾回收,通過調(diào)用 runtime.GC()检号,這是阻塞式的首懈。
// GC runs a garbage collection and blocks the caller until the
// garbage collection is complete. It may also block the entire program
func GC() {
...
gcStart(gcForceBlockMode, false)
...
}
GC 觸發(fā)條件
觸發(fā)條件主要關(guān)注下面代碼中的中間部分:
forceTrigger ||memstats.heap_live >= memstats.gc_trigger
forceTrigger 是 forceGC 的標(biāo)志;后面半句的意思是當(dāng)前堆上的活躍對象大于我們初始化時候設(shè)置的 GC 觸發(fā)閾值谨敛。在 malloc 以及 free 的時候 heap_live 會一直進(jìn)行更新,這里就不再展開了滤否。
// gcShouldStart returns true if the exit condition for the _GCoff
// phase has been met. The exit condition should be tested when
// allocating.
// If forceTrigger is true, it ignores the current heap size, but
// checks all other conditions. In general this should be false.
func gcShouldStart(forceTrigger bool) bool {
return gcphase == _GCoff && (forceTrigger || memstats.heap_live >= memstats.gc_trigger) && memstats.enablegc && panicking == 0 && gcpercent >= 0
}
//初始化的時候設(shè)置 GC 的觸發(fā)閾值
func gcinit() {
_ = setGCPercent(readgogc())
memstats.gc_trigger = heapminimum
...
}
// 啟動的時候通過 GOGC 傳遞百分比 x
// 觸發(fā)閾值等于 x * defaultHeapMinimum (defaultHeapMinimum 默認(rèn)是 4M)
func readgogc() int32 {
p := gogetenv("GOGC")
if p == "off" {
return -1
}
if n, ok := atoi32(p); ok {
return n
}
return 100
}
垃圾回收的主要流程
三色標(biāo)記法脸狸,主要流程如下:
- 所有對象最開始都是白色。
- 從 root 開始找到所有可達(dá)對象,標(biāo)記為灰色炊甲,放入待處理隊列泥彤。
- 遍歷灰色對象隊列,將其引用對象標(biāo)記為灰色放入待處理隊列卿啡,自身標(biāo)記為黑色吟吝。
- 循環(huán)步驟3直到灰色隊列為空為止,此時所有引用對象都被標(biāo)記為黑色颈娜,所有不可達(dá)的對象依然為白色剑逃,白色的就是需要進(jìn)行回收的對象。
詳細(xì)過程可參見官方給出的圖示
關(guān)于上圖有幾點需要說明的是官辽。
- 首先從 root 開始遍歷蛹磺,root 包括全局指針和 goroutine 棧上的指針。
- mark 有兩個過程同仆。
2.1. 從 root 開始遍歷萤捆,標(biāo)記為灰色。遍歷灰色隊列俗批。
2.2. re-scan 全局指針和棧俗或。因為 mark 和用戶程序是并行的,所以在過程 1 的時候可能會有新的對象分配岁忘,這個時候就需要通過寫屏障(write barrier)記錄下來辛慰。re-scan 再完成檢查一下。 -
STW(Stop The World)有兩個過程臭觉。
3.1. 第一個是 GC 將要開始的時候昆雀,這個時候主要是一些準(zhǔn)備工作,比如 enable write barrier蝠筑。
3.2. 第二個過程就是上面提到的 re-scan 過程狞膘。如果這個時候沒有 stw,那么 mark 將無休止什乙。
另外針對上圖各個階段對應(yīng) GCPhase 如下:
- Off: _GCoff
- Stack scan ~ Mark: _GCmark
- Mark termination: _GCmarktermination
寫屏障 (write barrier)
關(guān)于 write barrier挽封,這里只簡單介紹一下。垃圾回收中的 write barrier 可以理解為編譯器在寫操作時特意插入的一段代碼臣镣,對應(yīng)的還有 read barrier辅愿。
為什么需要 write barrier?
很簡單,對于和用戶程序并發(fā)運行的垃圾回收算法忆某,用戶程序會一直修改內(nèi)存点待,所以需要記錄下來。
Golang 1.7 之前的 write barrier 使用的經(jīng)典的 Dijkstra-style insertion write barrier [Dijkstra ‘78]弃舒, STW 的主要耗時就在 stack re-scan 的過程癞埠。自 1.8 之后采用一種混合的 write barrier 方式 (Yuasa-style deletion write barrier [Yuasa ‘90] 和 Dijkstra-style insertion write barrier [Dijkstra ‘78])來避免 re-scan状原。具體的可以參考 17503-eliminate-rescan 。
標(biāo)記
下面的源碼還是基于 go1.8rc3苗踪。這個版本的 GC 代碼相比之前改動還是挺大的颠区,我們下面盡量只關(guān)注主流程。垃圾回收的代碼主要集中在函數(shù) gcStart()中通铲。
// gcStart 是 GC 的入口函數(shù)毕莱,根據(jù) gcMode 做處理。
// 1. gcMode == gcBackgroundMode(后臺運行颅夺,也就是并行), _GCoff -> _GCmark
// 2. 否則 GCoff -> _GCmarktermination朋截,這個時候就是主動 GC
func gcStart(mode gcMode, forceTrigger bool) {
...
}
- STW phase 1
在 GC 開始之前的準(zhǔn)備工作。
func gcStart(mode gcMode, forceTrigger bool) {
... //在后臺啟動 mark worker
if mode == gcBackgroundMode {
gcBgMarkStartWorkers()
}
... // Stop The World
systemstack(stopTheWorldWithSema)
...
if mode == gcBackgroundMode {
// GC 開始前的準(zhǔn)備工作
//處理設(shè)置 GCPhase碗啄,setGCPhase 還會 enable write barrier
setGCPhase(_GCmark)
gcBgMarkPrepare() // Must happen before assist enable.
gcMarkRootPrepare() // Mark all active tinyalloc blocks. Since we're
// allocating from these, they need to be black like
// other allocations. The alternative is to blacken
// the tiny block on every allocation from it, which
// would slow down the tiny allocator.
gcMarkTinyAllocs()
// Start The World
systemstack(startTheWorldWithSema)
} else {
...
}
}
- Mark
Mark 階段是并行的運行质和,通過在后臺一直運行 mark worker 來實現(xiàn)。
func gcStart(mode gcMode, forceTrigger bool) {
...
//在后臺啟動 mark worker
if mode == gcBackgroundMode {
gcBgMarkStartWorkers()
}
}
func gcBgMarkStartWorkers() {
// Background marking is performed by per-P G's. Ensure that
// each P has a background GC G.
for _, p := range &allp {
if p == nil || p.status == _Pdead {
break
}
if p.gcBgMarkWorker == 0 {
go gcBgMarkWorker(p)
notetsleepg(&work.bgMarkReady, -1)
noteclear(&work.bgMarkReady)
}
}
}
// gcBgMarkWorker 是一直在后臺運行的稚字,大部分時候是休眠狀態(tài)饲宿,通過 gcController 來調(diào)度
func gcBgMarkWorker(_p_ *p) {
for {
// 將當(dāng)前 goroutine 休眠,直到滿足某些條件
gopark(...)
...
// mark 過程
systemstack(func() {
// Mark our goroutine preemptible so its stack
// can be scanned. This lets two mark workers
// scan each other (otherwise, they would
// deadlock). We must not modify anything on
// the G stack. However, stack shrinking is
// disabled for mark workers, so it is safe to
// read from the G stack.
casgstatus(gp, _Grunning, _Gwaiting)
switch _p_.gcMarkWorkerMode {
default:
throw("gcBgMarkWorker: unexpected gcMarkWorkerMode")
case gcMarkWorkerDedicatedMode:
gcDrain(&_p_.gcw, gcDrainNoBlock|gcDrainFlushBgCredit)
case gcMarkWorkerFractionalMode:
gcDrain(&_p_.gcw, gcDrainUntilPreempt|gcDrainFlushBgCredit)
case gcMarkWorkerIdleMode:
gcDrain(&_p_.gcw, gcDrainIdle|gcDrainUntilPreempt|gcDrainFlushBgCredit)
}
casgstatus(gp, _Gwaiting, _Grunning)
})
...
}
}
Mark 階段的標(biāo)記代碼主要在函數(shù) gcDrain() 中實現(xiàn)胆描。
// gcDrain scans roots and objects in work buffers, blackening grey
// objects until all roots and work buffers have been drained.
func gcDrain(gcw *gcWork, flags gcDrainFlags) {
...
// Drain root marking jobs.
if work.markrootNext < work.markrootJobs {
for !(preemptible && gp.preempt) {
job := atomic.Xadd(&work.markrootNext, +1) - 1
if job >= work.markrootJobs {
break
}
markroot(gcw, job)
if idle && pollWork() {
goto done
}
}
}
// 處理 heap 標(biāo)記
// Drain heap marking jobs.
for !(preemptible && gp.preempt) {
...
//從灰色列隊中取出對象
var b uintptr
if blocking {
b = gcw.get()
} else {
b = gcw.tryGetFast()
if b == 0 {
b = gcw.tryGet()
}
}
if b == 0 {
// work barrier reached or tryGet failed.
break
}
//掃描灰色對象的引用對象瘫想,標(biāo)記為灰色,入灰色隊列
scanobject(b, gcw)
}
}
- Mark termination (STW phase 2)
mark termination 階段會 stop the world昌讲。函數(shù)實現(xiàn)在 gcMarkTermination()国夜。1.8 版本已經(jīng)不會再對 goroutine stack 進(jìn)行 re-scan 了。細(xì)節(jié)有點多短绸,這里不細(xì)說了车吹。
func gcMarkTermination() {
// World is stopped.
// Run gc on the g0 stack. We do this so that the g stack
// we're currently running on will no longer change. Cuts
// the root set down a bit (g0 stacks are not scanned, and
// we don't need to scan gc's internal state). We also
// need to switch to g0 so we can shrink the stack.
systemstack(func() {
gcMark(startTime)
// Must return immediately.
// The outer function's stack may have moved
// during gcMark (it shrinks stacks, including the
// outer function's stack), so we must not refer
// to any of its variables. Return back to the
// non-system stack to pick up the new addresses
// before continuing.
})
...
}
清掃
清掃相對來說就簡單很多了。
func gcSweep(mode gcMode) {
...
//阻塞式
if !_ConcurrentSweep || mode == gcForceBlockMode {
// Special case synchronous sweep.
...
// Sweep all spans eagerly.
for sweepone() != ^uintptr(0) {
sweep.npausesweep++
}
// Do an additional mProf_GC, because all 'free' events are now real as well.
mProf_GC()
mProf_GC()
return
}
// 并行式
// Background sweep.
lock(&sweep.lock)
if sweep.parked {
sweep.parked = false
ready(sweep.g, 0, true)
}
unlock(&sweep.lock)
}
對于并行式清掃醋闭,在 GC 初始化的時候就會啟動 bgsweep()窄驹,然后在后臺一直循環(huán)。
func bgsweep(c chan int) {
sweep.g = getg()
lock(&sweep.lock)
sweep.parked = true
c <- 1
goparkunlock(&sweep.lock, "GC sweep wait", traceEvGoBlock, 1)
for {
for gosweepone() != ^uintptr(0) {
sweep.nbgsweep++
Gosched()
}
lock(&sweep.lock)
if !gosweepdone() {
// This can happen if a GC runs between
// gosweepone returning ^0 above
// and the lock being acquired.
unlock(&sweep.lock)
continue
}
sweep.parked = true
goparkunlock(&sweep.lock, "GC sweep wait", traceEvGoBlock, 1)
}
}
func gosweepone() uintptr {
var ret uintptr
systemstack(func() {
ret = sweepone()
})
return ret
}
不管是阻塞式還是并行式证逻,都是通過 sweepone()函數(shù)來做清掃工作的乐埠。如果對于上篇文章Golang 內(nèi)存管理 熟悉的話,這個地方就很好理解囚企。內(nèi)存管理都是基于 span 的丈咐,mheap_ 是一個全局的變量,所有分配的對象都會記錄在 mheap_ 中龙宏。在標(biāo)記的時候棵逊,我們只要找到對對象對應(yīng)的 span 進(jìn)行標(biāo)記,清掃的時候掃描 span银酗,沒有標(biāo)記的 span 就可以回收了辆影。
// sweeps one span
// returns number of pages returned to heap, or ^uintptr(0) if there is nothing to sweep.
func sweepone() uintptr {
...
for {
s := mheap_.sweepSpans[1-sg/2%2].pop()
...
if !s.sweep(false) {
// Span is still in-use, so this returned no
// pages to the heap and the span needs to
// move to the swept in-use list.
npages = 0
}
}
}
// Sweep frees or collects finalizers for blocks not marked in the mark phase.
// It clears the mark bits in preparation for the next GC round.
// Returns true if the span was returned to heap.
// If preserve=true, don't return it to heap nor relink in MCentral lists;
// caller takes care of it.
func (s *mspan) sweep(preserve bool) bool {
...
}
其他
- gcWork
這里介紹一下任務(wù)隊列掩浙,或者說灰色對象管理。每個 P 上都有一個 gcw 用來管理灰色對象(get 和 put)秸歧,gcw 的結(jié)構(gòu)就是 gcWork。gcWork 中的核心是 wbuf1 和 wbuf2衅澈,里面存儲就是灰色對象键菱,或者說是 work(下面就全部統(tǒng)一叫做 work)。
type p struct {
...
gcw gcWork
}type gcWork struct {
// wbuf1 and wbuf2 are the primary and secondary work buffers.
wbuf1, wbuf2 wbufptr
// Bytes marked (blackened) on this gcWork. This is aggregated
// into work.bytesMarked by dispose.
bytesMarked uint64
// Scan work performed on this gcWork. This is aggregated into
// gcController by dispose and may also be flushed by callers.
scanWork int64
}
既然每個 P 上有一個 work buffer今布,那么是不是還有一個全局的 work list 呢经备?是的。通過在每個 P 上綁定一個 work buffer 的好處和 cache 一樣部默,不需要加鎖侵蒙。
var work struct {
full uint64 // lock-free list of full blocks workbuf
empty uint64 // lock-free list of empty blocks workbuf
pad0 [sys.CacheLineSize]uint8 // prevents false-sharing between full/empty and nproc/nwait
...
}
那么為什么使用兩個 work buffer (wbuf1 和 wbuf2)呢?我下面舉個例子傅蹂。比如我現(xiàn)在要 get 一個 work 出來纷闺,先從 wbuf1 中取,wbuf1 為空的話則與 wbuf2 swap 再 get份蝴。在其他時間將 work buffer 中的 full 或者 empty buffer 移到 global 的 work 中犁功。這樣的好處在于,在 get 的時候去全局的 work 里面然榉颉(多個 goroutine 去取會有競爭)浸卦。這里有趣的是 global 的 work list 是 lock-free 的,通過原子操作 cas 等實現(xiàn)案糙。下面列舉幾個函數(shù)看一下 gcWrok限嫌。
初始化
func (w *gcWork) init() {
w.wbuf1 = wbufptrOf(getempty())
wbuf2 := trygetfull()
if wbuf2 == nil {
wbuf2 = getempty()
}
w.wbuf2 = wbufptrOf(wbuf2)
}
put
// put enqueues a pointer for the garbage collector to trace.
// obj must point to the beginning of a heap object or an oblet.
func (w *gcWork) put(obj uintptr) {
wbuf := w.wbuf1.ptr()
if wbuf == nil {
w.init()
wbuf = w.wbuf1.ptr() // wbuf is empty at this point.
} else if wbuf.nobj == len(wbuf.obj) {
w.wbuf1, w.wbuf2 = w.wbuf2, w.wbuf1
wbuf = w.wbuf1.ptr()
if wbuf.nobj == len(wbuf.obj) {
putfull(wbuf)
wbuf = getempty()
w.wbuf1 = wbufptrOf(wbuf)
flushed = true
}
}
wbuf.obj[wbuf.nobj] = obj
wbuf.nobj++
}
get
// get dequeues a pointer for the garbage collector to trace, blocking
// if necessary to ensure all pointers from all queues and caches have
// been retrieved. get returns 0 if there are no pointers remaining.
//go:nowritebarrier
func (w *gcWork) get() uintptr {
wbuf := w.wbuf1.ptr()
if wbuf == nil {
w.init()
wbuf = w.wbuf1.ptr() // wbuf is empty at this point.
}
if wbuf.nobj == 0 {
w.wbuf1, w.wbuf2 = w.wbuf2, w.wbuf1
wbuf = w.wbuf1.ptr()
if wbuf.nobj == 0 {
owbuf := wbuf
wbuf = getfull()
if wbuf == nil {
return 0
}
putempty(owbuf)
w.wbuf1 = wbufptrOf(wbuf)
}
}
// TODO: This might be a good place to add prefetch code
wbuf.nobj--
return wbuf.obj[wbuf.nobj]
}
- forcegc
我們上面講了兩種 GC 觸發(fā)方式:自動檢測和用戶主動調(diào)用。除此之后 Golang 本身還會對運行狀態(tài)進(jìn)行監(jiān)控时捌,如果超過兩分鐘沒有 GC怒医,則觸發(fā) GC。監(jiān)控函數(shù)是 sysmon()匣椰,在主 goroutine 中啟動裆熙。
// The main goroutine
func main() {
...
systemstack(func() {
newm(sysmon, nil)
})
}
// Always runs without a P, so write barriers are not allowed.
func sysmon() {
...
for {
now := nanotime()
unixnow := unixnanotime()
lastgc := int64(atomic.Load64(&memstats.last_gc))
if gcphase == _GCoff && lastgc != 0 && unixnow-lastgc > forcegcperiod && atomic.Load(&forcegc.idle) != 0 {
lock(&forcegc.lock)
forcegc.idle = 0
forcegc.g.schedlink = 0
injectglist(forcegc.g) // 將 forcegc goroutine 加入 runnable queue
unlock(&forcegc.lock)
}
}
}
var forcegcperiod int64 = 2 * 60 *1e9 //兩分鐘
程序的優(yōu)化
現(xiàn)在我們已經(jīng)了解了golang中的垃圾回收機(jī)制
那么如何從代碼方面優(yōu)化以減少gc導(dǎo)致的STW的時間?
減少對象的分配
使用sync.Pool
說明
- 對于golang gc的時候禽笑,過程是:掃描-標(biāo)記-清除入录,這3個步驟中在程序中能做的就是減少對象的分配,直觀的結(jié)果就是減少了gc的掃描和標(biāo)記時間佳镜,而我們已經(jīng)知道m(xù)ark階段是會導(dǎo)致stw的僚稿,最終結(jié)果直接導(dǎo)致stw的時間減少。
- sync.Pool有兩個特性
2.1. 能有效分擔(dān)對象存儲壓力
2.2. 對gc友好
如何減少對象的分配蟀伸?
—————待更—————