在早期經(jīng)常遭到唾棄的就是在垃圾回收(下稱:GC)機制中 STW(Stop-The-World)的時間過長。那么這個時候,我們又會好奇一點,作為 STW 的起始,Go 語言中什么時候才會觸發(fā) GC 呢?
1、什么是 GC
在計算機科學(xué)中,垃圾回收(GC)是一種自動管理內(nèi)存的機制,垃圾回收器會去嘗試回收程序不再使用的對象及其占用的內(nèi)存。
最早 John McCarthy
在 1959 年左右發(fā)明了垃圾回收,以簡化 Lisp
中的手動內(nèi)存管理的機制(來自 @wikipedia)。
2、為什么要 GC
手動管理內(nèi)存挺麻煩,管錯或者管漏內(nèi)存也很糟糕,將會直接導(dǎo)致程序不穩(wěn)定(持續(xù)泄露)甚至直接崩潰。
3、GC 觸發(fā)場景
GC 觸發(fā)的場景主要分為兩大類,分別是:
- 系統(tǒng)觸發(fā):運行時自行根據(jù)內(nèi)置的條件,檢查、發(fā)現(xiàn)到,則進行 GC 處理,維護整個應(yīng)用程序的可用性。
-
手動觸發(fā):開發(fā)者在業(yè)務(wù)代碼中自行調(diào)用
runtime.GC
方法來觸發(fā) GC 行為。
3.1系統(tǒng)觸發(fā)
在系統(tǒng)觸發(fā)的場景中,Go 源碼的 src/runtime/mgc.go
文件,明確標(biāo)識了 GC 系統(tǒng)觸發(fā)的三種場景,分別如下:
1
2
3
4
5
|
const ( gcTriggerHeap gcTriggerKind = iota gcTriggerTime gcTriggerCycle ) |
-
gcTriggerHeap:
當(dāng)所分配的堆大小達到閾值(由控制器計算的觸發(fā)堆的大小)時,將會觸發(fā)。 -
gcTriggerTime:
當(dāng)距離上一個 GC 周期的時間超過一定時間時,將會觸發(fā)。-時間周期以runtime.forcegcperiod
變量為準(zhǔn),默認(rèn) 2 分鐘。 -
gcTriggerCycle:
如果沒有開啟 GC,則啟動 GC。
在手動觸發(fā)的 runtime.GC
方法中涉及。
3.2手動觸發(fā)
在手動觸發(fā)的場景下,Go 語言中僅有 runtime.GC
方法可以觸發(fā),也就沒什么額外的分類的。
但我們要思考的是,一般我們在什么業(yè)務(wù)場景中,要涉及到手動干涉 GC,強制觸發(fā)他呢?
需要手動強制觸發(fā)的場景極其少見,可能會是在某些業(yè)務(wù)方法執(zhí)行完后,因其占用了過多的內(nèi)存,需要人為釋放。又或是 debug
程序所需。
3.3 基本流程
在了解到 Go 語言會觸發(fā) GC 的場景后,我們進一步看看觸發(fā) GC 的流程代碼是怎么樣的,我們可以借助手動觸發(fā)的 runtime.GC
方法來作為突破口。
核心代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
func GC() { n := atomic.Load(&work.cycles) gcWaitOnMark(n) gcStart(gcTrigger{kind: gcTriggerCycle, n: n + 1}) gcWaitOnMark(n + 1) for atomic.Load(&work.cycles) == n+1 && sweepone() != ^uintptr(0) { sweep.nbgsweep++ Gosched() } for atomic.Load(&work.cycles) == n+1 && atomic.Load(&mheap_.sweepers) != 0 { Gosched() } mp := acquirem() cycle := atomic.Load(&work.cycles) if cycle == n+1 || (gcphase == _GCmark && cycle == n+2) { mProf_PostSweep() } releasem(mp) } |
在開始新的一輪 GC 周期前,需要調(diào)用 gcWaitOnMark
方法上一輪 GC 的標(biāo)記結(jié)束(含掃描終止、標(biāo)記、或標(biāo)記終止等)。
開始新的一輪 GC 周期,調(diào)用 gcStart
方法觸發(fā) GC 行為,開始掃描標(biāo)記階段。
需要調(diào)用 gcWaitOnMark
方法等待,直到當(dāng)前 GC 周期的掃描、標(biāo)記、標(biāo)記終止完成。
需要調(diào)用 sweepone
方法,掃描未掃除的堆跨度,并持續(xù)掃除,保證清理完成。在等待掃除完畢前的阻塞時間,會調(diào)用 Gosched
讓出。
在本輪 GC 已經(jīng)基本完成后,會調(diào)用 mProf_PostSweep 方法。以此記錄最后一次標(biāo)記終止時的堆配置文件快照。
結(jié)束,釋放 M。
3.4 在哪觸發(fā)
看完 GC 的基本流程后,我們有了一個基本的了解。但可能又有小伙伴有疑惑了?
本文的標(biāo)題是 “GC 什么時候會觸發(fā) GC”,雖然我們前面知道了觸發(fā)的時機。但是....Go 是哪里實現(xiàn)的觸發(fā)的機制,似乎在流程中完全沒有看到?
4、監(jiān)控線程
實質(zhì)上在 Go 運行時(runtime)
初始化時,會啟動一個 goroutine
,用于處理 GC 機制的相關(guān)事項。
代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
func init() { go forcegchelper() } func forcegchelper() { forcegc.g = getg() lockInit(&forcegc.lock, lockRankForcegc) for { lock(&forcegc.lock) if forcegc.idle != 0 { throw( "forcegc: phase error" ) } atomic.Store(&forcegc.idle, 1) goparkunlock(&forcegc.lock, waitReasonForceGCIdle, traceEvGoBlock, 1) // this goroutine is explicitly resumed by sysmon if debug.gctrace > 0 { println( "GC forced" ) } gcStart(gcTrigger{kind: gcTriggerTime, now: nanotime()}) } } |
在這段程序中,需要特別關(guān)注的是在 forcegchelper
方法中,會調(diào)用 goparkunlock
方法讓該 goroutine
陷入休眠等待狀態(tài),以減少不必要的資源開銷。
在休眠后,會由 sysmon
這一個系統(tǒng)監(jiān)控線程來進行監(jiān)控、喚醒等行為:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
func sysmon() { ... for { ... // check if we need to force a GC if t := (gcTrigger{kind: gcTriggerTime, now: now}); t. test () && atomic.Load(&forcegc.idle) != 0 { lock(&forcegc.lock) forcegc.idle = 0 var list gList list.push(forcegc.g) injectglist(&list) unlock(&forcegc.lock) } if debug.schedtrace > 0 && lasttrace+int64(debug.schedtrace)*1000000 <= now { lasttrace = now schedtrace(debug.scheddetail > 0) } unlock(&sched.sysmonlock) } } |
這段代碼核心的行為就是不斷地在 for 循環(huán)中,對 gcTriggerTime
和 now
變量進行比較,判斷是否達到一定的時間(默認(rèn)為 2 分鐘)。
若達到意味著滿足條件,會將 forcegc.g
放到全局隊列中接受新的一輪調(diào)度,再進行對上面 forcegchelper
的喚醒。
5、堆內(nèi)存申請
在了解定時觸發(fā)的機制后,另外一個場景就是分配的堆空間的時候,那么我們要看的地方就非常明確了。
那就是運行時申請堆內(nèi)存的 mallocgc
方法。核心代碼如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { shouldhelpgc := false ... if size <= maxSmallSize { if noscan && size < maxTinySize { ... // Allocate a new maxTinySize block. span = c.alloc[tinySpanClass] v := nextFreeFast(span) if v == 0 { v , span, shouldhelpgc = c.nextFree(tinySpanClass) } ... spc := makeSpanClass(sizeclass, noscan) span = c.alloc[spc] v := nextFreeFast(span) if v == 0 { v , span, shouldhelpgc = c.nextFree(spc) } ... } } else { shouldhelpgc = true span = c.allocLarge(size, needzero, noscan) ... } if shouldhelpgc { if t := (gcTrigger{kind: gcTriggerHeap}); t. test () { gcStart(t) } } return x } |
小對象:如果申請小對象時,發(fā)現(xiàn)當(dāng)前內(nèi)存空間不存在空閑跨度時,將會需要調(diào)用 nextFree
方法獲取新的可用的對象,可能會觸發(fā) GC 行為。
大對象:如果申請大于 32k 以上的大對象時,可能會觸發(fā) GC 行為。
總結(jié)
在這篇文章中,我們介紹了 Go 語言觸發(fā) GC 的兩大類場景,并分別基于大類中的細(xì)分場景進行了一一說明。
到此這篇關(guān)于談?wù)揋o 什么時候會觸發(fā) GC問題的文章就介紹到這了,更多相關(guān)Go 什么時候會觸發(fā) GC?內(nèi)容請搜索服務(wù)器之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持服務(wù)器之家!
原文鏈接:https://www.tuicool.com/articles/2aua2mf