更好的閱讀體驗建議點擊下方原文鏈接。
原文地址:http://maoqide.live/post/golang/golang-gc-memory-allocation/
關於 Golang GC 和內存管理相關的流程和原理的一些總結。
GC 流程
golang GC 采用基於標記-清除的三色標記法,下圖為 golang 一輪完整的 GC 的過程:
一輪完整的 GC,總是從 Off,如果不是 Off 狀態,則代表上一輪GC還未完成,如果這時修改指針的值,是直接修改的。
Stack scan: 收集根對象(全局變量和 goroutine 棧上的變量),該階段會開啟寫屏障(Write Barrier)。
Mark: 標記對象,直到標記完所有根對象和根對象可達對象。此時寫屏障會記錄所有指針的更改(通過 mutator)。
Mark Termination: 重新掃描部分全局變量和發生更改的棧變量,完成標記,該階段會STW(Stop The World),也是 gc 時造成 go 程序停頓的主要階段。
Sweep: 並發的清除未標記的對象。
三色標記
以上 Mark 階段,采用的是三色標記法,是傳統標記-清除算法的一種優化,主要思想是增加了一種中間狀態,即灰色對象,以減少 STW 時間。
三色標記將對象分為黑色、白色、灰色三種:
- 黑色:已標記的對象,表示對象是根對象可達的。
- 白色:未標記對象,gc開始時所有對象為白色,當gc結束時,如果仍為白色,說明對象不可達,在 sweep 階段會被清除。
- 灰色:被黑色對象引用到的對象,但其引用的自對象還未被掃描,灰色為標記過程的中間狀態,當灰色對象全部被標記完成代表本次標記階段結束。
三色標記的主要過程即:
- 開始時所有對象為白色
- 將所有根對象標記為灰色,放入隊列
- 遍歷灰色對象,將其標記為黑色,並將他們引用的對象標記為灰色,放入隊列
- 重復步驟 3 持續遍歷灰色對象,直至隊列為空
- 此時只剩下黑色對象和白色對象,白色對象即為下一步需要清除的對象
STW
傳統的標記-清除算法,為了防止在標記過程中,對象引用發生變化,導致清除仍在使用的對象,需要 STW(Stop The World),這會造成程序的停頓。在三色標記的過程中,由於引入了灰色對象這一中間狀態,標記過程和用戶的 golang 代碼中可以並發執行,不需要 STW,這極大的減少了應用的停頓時間。
三色標記具體如何避免在標記過程中對象應用的改變呢,這里用到了寫屏障(Write Barrier)。
寫屏障
在 GC 的流程中,Stack scan 這一步驟,啟用了寫屏障。寫屏障的主要思想,是在標記的過程中,通過寫屏障記錄發生變化的指針,然后在 Mark termination 的 rescan 過程中,重新進行掃描,因為在這一步驟會 STW,所以在這一步驟完成后的白色對象,不會再被引用,可以直接清除。關於寫屏障具體原理和實現,這里不再展開。
GC觸發
golang 程序的執行過程中,如下幾種情況下會觸發 GC:
- 主動觸發,用戶代碼中調用
runtime.GC
會主動觸發 GC - 默認每 2min 未產生 GC 時,golang 的守護協程 sysmon 會強制觸發 GC
- 當 go 程序分配的內存增長超過閾值時,會觸發 GC
內存分配
golang 內存分配分為堆內存和棧內存。
棧:一般函數內部執行中聲明的變量,函數返回直接釋放,不會引起垃圾回收,對性能無影響。
堆:有引用到的內存空間,靠 GC 回收,會影響程序進程。
內存逃逸
逃逸分析是指由編譯器決定內存分配的位置,不需要程序員指定。即由編譯器決定新申請的對象會分配到堆上還是棧上。
逃逸分析場景:
- 指針逃逸
go 將函數內定義的變量返回到函數外,會將本應分配到棧上的內存分配到堆上。 - 棧空間不足逃逸
當棧空間不足或無法判斷當前切片長度時會將對象分配到堆上。 - 動態類型逃逸
當函數參數為 interface 類型,編譯期間無法確定參數的具體類型,也可能會產生逃逸。