Go 什麼時候會觸發 GC?

大家好,我是煎魚。

Go 語言作爲一門新語言,在早期經常遭到唾棄的就是在垃圾回收(下稱:GC)機制中 STW(Stop-The-World)的時間過長。

那麼這個時候,我們又會好奇一點,作爲 STW 的起始,Go 語言中什麼時候纔會觸發 GC 呢?

今天就由煎魚帶大家一起來學習研討一輪。

什麼是 GC

在計算機科學中,垃圾回收(GC)是一種自動管理內存的機制,垃圾回收器會去嘗試回收程序不再使用的對象及其佔用的內存。

最早 John McCarthy 在 1959 年左右發明了垃圾回收,以簡化 Lisp 中的手動內存管理的機制(來自 @wikipedia)。

圖來自網絡

爲什麼要 GC

手動管理內存挺麻煩,管錯或者管漏內存也很糟糕,將會直接導致程序不穩定(持續泄露)甚至直接崩潰。

GC 觸發場景

GC 觸發的場景主要分爲兩大類,分別是:

  1. 系統觸發:運行時自行根據內置的條件,檢查、發現到,則進行 GC 處理,維護整個應用程序的可用性。

  2. 手動觸發:開發者在業務代碼中自行調用 runtime.GC 方法來觸發 GC 行爲。

系統觸發

在系統觸發的場景中,Go 源碼的 src/runtime/mgc.go 文件,明確標識了 GC 系統觸發的三種場景,分別如下:

const (
 gcTriggerHeap gcTriggerKind = iota
 gcTriggerTime
 gcTriggerCycle
)

手動觸發

在手動觸發的場景下,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)
}
  1. 在開始新的一輪 GC 週期前,需要調用 gcWaitOnMark 方法上一輪 GC 的標記結束(含掃描終止、標記、或標記終止等)。

  2. 開始新的一輪 GC 週期,調用 gcStart 方法觸發 GC 行爲,開始掃描標記階段。

  3. 需要調用 gcWaitOnMark 方法等待,直到當前 GC 週期的掃描、標記、標記終止完成。

  4. 需要調用 sweepone 方法,掃描未掃除的堆跨度,並持續掃除,保證清理完成。在等待掃除完畢前的阻塞時間,會調用 Gosched 讓出。

  5. 在本輪 GC 已經基本完成後,會調用 mProf_PostSweep 方法。以此記錄最後一次標記終止時的堆配置文件快照。

  6. 結束,釋放 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 循環中,對 gcTriggerTimenow 變量進行比較,判斷是否達到一定的時間(默認爲 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
}

總結

在這篇文章中,我們介紹了 Go 語言觸發 GC 的兩大類場景,並分別基於大類中的細分場景進行了一一說明。

一般來講,我們對其瞭解大概就可以了。若小夥伴們對其內部具體實現感興趣,也可以以文章中的代碼具體再打開看。

但需要注意,很有可能 Go 版本一升級,可能又變了,學思想要緊!

你好,我是煎魚。高一折騰過前端,參加過國賽拿了獎,大學搞過 PHP。現在整 Go,在公司負責微服務架構等相關工作推進和研發。

從大學開始靠自己賺生活費和學費,到出版 Go 暢銷書《Go 語言編程之旅》,再到獲得 GOP(Go 領域最有觀點專家)榮譽,點擊藍字查看我的出書之路

日常分享高質量文章,輸出 Go 面試、工作經驗、架構設計,加微信拉讀者交流羣,記得點贊!

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/e2-NXWCS0bd2BPWzdeiS_A