Go 什麼時候會觸發 GC?
大家好,我是煎魚。
Go 語言作爲一門新語言,在早期經常遭到唾棄的就是在垃圾回收(下稱:GC)機制中 STW(Stop-The-World)的時間過長。
那麼這個時候,我們又會好奇一點,作爲 STW 的起始,Go 語言中什麼時候纔會觸發 GC 呢?
今天就由煎魚帶大家一起來學習研討一輪。
什麼是 GC
在計算機科學中,垃圾回收(GC)是一種自動管理內存的機制,垃圾回收器會去嘗試回收程序不再使用的對象及其佔用的內存。
最早 John McCarthy 在 1959 年左右發明了垃圾回收,以簡化 Lisp 中的手動內存管理的機制(來自 @wikipedia)。
圖來自網絡
爲什麼要 GC
手動管理內存挺麻煩,管錯或者管漏內存也很糟糕,將會直接導致程序不穩定(持續泄露)甚至直接崩潰。
GC 觸發場景
GC 觸發的場景主要分爲兩大類,分別是:
-
系統觸發:運行時自行根據內置的條件,檢查、發現到,則進行 GC 處理,維護整個應用程序的可用性。
-
手動觸發:開發者在業務代碼中自行調用
runtime.GC
方法來觸發 GC 行爲。
系統觸發
在系統觸發的場景中,Go 源碼的 src/runtime/mgc.go
文件,明確標識了 GC 系統觸發的三種場景,分別如下:
const (
gcTriggerHeap gcTriggerKind = iota
gcTriggerTime
gcTriggerCycle
)
-
gcTriggerHeap:當所分配的堆大小達到閾值(由控制器計算的觸發堆的大小)時,將會觸發。
-
gcTriggerTime:當距離上一個 GC 週期的時間超過一定時間時,將會觸發。- 時間週期以
runtime.forcegcperiod
變量爲準,默認 2 分鐘。 -
gcTriggerCycle:如果沒有開啓 GC,則啓動 GC。
-
在手動觸發的
runtime.GC
方法中涉及。
手動觸發
在手動觸發的場景下,Go 語言中僅有 runtime.GC
方法可以觸發,也就沒什麼額外的分類的。
但我們要思考的是,一般我們在什麼業務場景中,要涉及到手動干涉 GC,強制觸發他呢?
需要手動強制觸發的場景極其少見,可能會是在某些業務方法執行完後,因其佔用了過多的內存,需要人爲釋放。又或是 debug 程序所需。
基本流程
在瞭解到 Go 語言會觸發 GC 的場景後,我們進一步看看觸發 GC 的流程代碼是怎麼樣的,我們可以藉助手動觸發的 runtime.GC
方法來作爲突破口。
核心代碼如下:
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 週期前,需要調用
gcWaitOnMark
方法上一輪 GC 的標記結束(含掃描終止、標記、或標記終止等)。 -
開始新的一輪 GC 週期,調用
gcStart
方法觸發 GC 行爲,開始掃描標記階段。 -
需要調用
gcWaitOnMark
方法等待,直到當前 GC 週期的掃描、標記、標記終止完成。 -
需要調用
sweepone
方法,掃描未掃除的堆跨度,並持續掃除,保證清理完成。在等待掃除完畢前的阻塞時間,會調用Gosched
讓出。 -
在本輪 GC 已經基本完成後,會調用
mProf_PostSweep
方法。以此記錄最後一次標記終止時的堆配置文件快照。 -
結束,釋放 M。
在哪觸發
看完 GC 的基本流程後,我們有了一個基本的瞭解。但可能又有小夥伴有疑惑了?
本文的標題是 “GC 什麼時候會觸發 GC”,雖然我們前面知道了觸發的時機。但是....Go 是哪裏實現的觸發的機制,似乎在流程中完全沒有看到?
監控線程
實質上在 Go 運行時(runtime)初始化時,會啓動一個 goroutine,用於處理 GC 機制的相關事項。
代碼如下:
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()})
}
}
在這段程序中,需要特別關注的是在 forcegchelper
方法中,會調用 goparkunlock
方法讓該 goroutine 陷入休眠等待狀態,以減少不必要的資源開銷。
在休眠後,會由 sysmon
這一個系統監控線程來進行監控、喚醒等行爲:
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 循環中,對 gcTriggerTime
和 now
變量進行比較,判斷是否達到一定的時間(默認爲 2 分鐘)。
若達到意味着滿足條件,會將 forcegc.g
放到全局隊列中接受新的一輪調度,再進行對上面 forcegchelper
的喚醒。
堆內存申請
在瞭解定時觸發的機制後,另外一個場景就是分配的堆空間的時候,那麼我們要看的地方就非常明確了。
那就是運行時申請堆內存的 mallocgc
方法。核心代碼如下:
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
}
-
小對象:如果申請小對象時,發現當前內存空間不存在空閒跨度時,將會需要調用
nextFree
方法獲取新的可用的對象,可能會觸發 GC 行爲。 -
大對象:如果申請大於 32k 以上的大對象時,可能會觸發 GC 行爲。
總結
在這篇文章中,我們介紹了 Go 語言觸發 GC 的兩大類場景,並分別基於大類中的細分場景進行了一一說明。
一般來講,我們對其瞭解大概就可以了。若小夥伴們對其內部具體實現感興趣,也可以以文章中的代碼具體再打開看。
但需要注意,很有可能 Go 版本一升級,可能又變了,學思想要緊!
你好,我是煎魚。高一折騰過前端,參加過國賽拿了獎,大學搞過 PHP。現在整 Go,在公司負責微服務架構等相關工作推進和研發。
從大學開始靠自己賺生活費和學費,到出版 Go 暢銷書《Go 語言編程之旅》,再到獲得 GOP(Go 領域最有觀點專家)榮譽,點擊藍字查看我的出書之路。
日常分享高質量文章,輸出 Go 面試、工作經驗、架構設計,加微信拉讀者交流羣,記得點贊!
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/e2-NXWCS0bd2BPWzdeiS_A