Categories
程式開發

淺析Golang 垃圾回收機制


前言

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

垃圾回收算法

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

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

代表語言:Python、PHP、Swift優點:對象回收快,不會出現內存耗盡或達到某個閾值時才回收。缺點:不能很好的處理循環引用,而實時維護引用計數也是有損耗的。

標記-清除:從根變量開始遍歷所有引用的對象,標記引用的對象,沒有被標記的進行回收。

代表語言:Golang(三色標記法)優點:解決了引用計數的缺點。缺點:需要STW,暫時停掉程序運行。

分代收集:按照對像生命週期長短劃分不同的代空間,生命週期長的放入老年代,短的放入新生代,不同代有不同的回收算法和回收頻率。

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

Golang 垃圾回收

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

三色標記法

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

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

具體流程如下圖:

淺析Golang 垃圾回收機制 1

回收原理

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

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

內存標記

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

淺析Golang 垃圾回收機制 2

如上圖,allocBits 記錄了每塊內存的分配情況,而 gcmarkBits 記錄了每塊內存的標記情況。在標記階段會對每塊內存進行標記,有對象引用的內存標記為1,沒有對象引用的為0。而 allocBits 和 gcmarkBits 的數據結構是完全一樣的,在結束標記後,將 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 做一部分的工作。

垃圾回收觸發機制

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

GC 優化建議

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

結語

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

淺析Golang 垃圾回收機制 3