開發與維運

淺析 Golang 垃圾回收機制

前言

Google 搜索 Golang GC 排名靠前的文章都講的不錯,從設計到實現,從演進到源碼,一應俱全。但是龐雜的信息會給人一種恐懼感,讓人望而卻步。本文嘗試使用較為簡單易懂的語言和圖像,講解 Golang 的垃圾回收機制。

垃圾回收算法

目前比較常見的垃圾回收算法有三種:

  1. 引用計數:為每個對象維護一個引用計數,當引用該對象的對象銷燬時,引用計數 -1,當對象引用計數為 0 時回收該對象。

    • 代表語言:PythonPHPSwift
    • 優點:對象回收快,不會出現內存耗盡或達到某個閾值時才回收。
    • 缺點:不能很好的處理循環引用,而實時維護引用計數也是有損耗的。
  2. 標記-清除:從根變量開始遍歷所有引用的對象,標記引用的對象,沒有被標記的進行回收。

    • 代表語言:Golang(三色標記法)
    • 優點:解決了引用計數的缺點。
    • 缺點:需要 STW,暫時停掉程序運行。
  3. 分代收集:按照對象生命週期長短劃分不同的代空間,生命週期長的放入老年代,短的放入新生代,不同代有不同的回收算法和回收頻率。

    • 代表語言:Java
    • 優點:回收性能好
    • 缺點:算法複雜

Golang 垃圾回收

跳過原理,我們先來介紹 Golang 的三色標記法。

三色標記法

三色標記法只是為了敘述方便而抽象出來的一種說法,實際上的對象是沒有三色之分的。這裡的三色,對應了垃圾回收過程中對象的三種狀態:

  • 灰色:對象還在標記隊列中等待
  • 黑色:對象已被標記,gcmarkBits 對應位為 1 -- 該對象不會在本次 GC 中被回收
  • 白色:對象未被標記,gcmarkBits 對應位為 0 -- 該對象將會在本次 GC 中被清理

具體流程如下圖:

三色標記法

回收原理

通過上圖,應該對三色標記法有了一個比較直觀的瞭解,那麼我們現在來講講原理。簡單的講,就是標記內存中那些還在使用中(即被引用了)的部分,而內存中不再使用(即未被引用)的部分,就是要回收的垃圾,需要將其回收,以供後續內存分配使用。上圖中的 A、B、D 就是被引用正在使用的內存,而C、F、E 曾經被使用過,但現在沒有任何對象引用,就需要被回收掉。

而 Root 區域主要是程序運行到當前時刻的棧和全局數據區域,是實時正在使用到的內存,當然應該優先標記。而考慮到內存塊中存放的可能是指針,所以還需要遞歸的進行標記,待全部標記完後,就會對未被標記的內存進行回收。

內存標記

golang 中採用 span 數據結構管理內存,span 中維護了一個個內存塊,並由一個位圖 allocBits 表示內存塊的分配情況,而上文中提到的 gcmarkBits 是記錄每塊內存塊被引用情況的。

內存標記

如上圖,allocBits 記錄了每塊內存的分配情況,而 gcmarkBits 記錄了每塊內存的標記情況。在標記階段會對每塊內存進行標記,有對象引用的內存標記為 1,沒有對象引用的為 0。而 allocBitsgcmarkBits 的數據結構是完全一樣的,在結束標記後,將 allocBits 指向 gcmarkBits,則有標記的才是存活的,這樣就完成了內存回收。而 gcmarkBits 則會在下次標記時重新分配內存。

垃圾回收優化

在前文中提到,golang 的垃圾回收算法屬於 標記-清除,是需要 STW 的。STW 就是 Stop The World 的意思,在 golang 中就是要停掉所有的 goroutine,專心進行垃圾回收,待垃圾回收結束後再恢復 goroutine。而 STW 時間的長短直接影響了應用的執行,如果時間過長,那將是災難性的。為了縮短 STW 時間,golang 不對優化垃圾回收算法,其中寫屏障(Write Barrier)輔助GC(Mutator Assist)就是兩種優化垃圾回收的方法。

  • 寫屏障(Write Barrier):上面說到的 STW 的目的是防止 GC 掃描時內存變化引起的混亂,而寫屏障就是讓 goroutine 與 GC 同時運行的手段,雖然不能完全消除 STW,但是可以大大減少 STW 的時間。寫屏障在 GC 的特定時間開啟,開啟後指針傳遞時會把指針標記,即本輪不回收,下次 GC 時再確定。
  • 輔助 GC(Mutator Assist):為了防止內存分配過快,在 GC 執行過程中,GC 過程中 mutator 線程會併發運行,而 mutator assist 機制會協助 GC 做一部分的工作。

垃圾回收觸發機制

  1. 內存分配量達到閾值:每次內存分配都會檢查當前內存分配量是否達到閾值,如果達到閾值則觸發 GC。閾值 = 上次 GC 內存分配量 * 內存增長率,內存增長率由環境變量 GOGC 控制,默認為 100,即每當內存擴大一倍時啟動 GC。
  2. 定時觸發 GC:默認情況下,2分鐘觸發一次 GC,該間隔由 src/runtime/proc.go 中的 forcegcperiod 聲明。
  3. 手動觸發 GC:在代碼中,可通過使用 runtime.GC() 手動觸發 GC。

GC 優化建議

由上文可知,GC 性能是與對象數量有關的,對象越多 GC 性能越差,對程序的影響也越大。所以在開發中要儘量減少對象分配個數,採用對象複用、將小對象組合成大對象或採用小數據類型(如使用 int8 代替 int)等。

結語

一門編程語言的垃圾回收機制會直接影響使用其開發應用的性能。在日常開發工作中也因注意到其作用,有助於開發出高性能的應用,這也是 GC 常常在面試中被問到的原因。同時,瞭解 GC 對了解內存管理也很有幫助。

歡迎掃描二維碼關注公眾號,瞭解更多雲原生知識

Leave a Reply

Your email address will not be published. Required fields are marked *