一文搞懂 go gc 垃圾回收原理
什麼是垃圾回收
我們在程序中定義一個變量,會在內存中開闢相應內存空間進行存儲,當不需要此變量後,需要手動銷燬此對象,並釋放內存。而這種對不再使用的內存資源進行自動回收的功能即爲垃圾回收(Garbage Collection,縮寫爲 GC),是一種自動內存管理機制
如何識別垃圾
引用計數算法 (reference counting)
引用計數通過在對象上增加自己被引用的次數,被其他對象引用時加 1,引用自己的對象被回收時減 1,引用數爲 0 的對象即爲可以被回收的對象,這種算法在內存比較緊張和實時性比較高的系統中使用比較廣泛,如 php,Python 等。
優點:
- 方式簡單,回收速度快。
缺點:
-
需要額外的空間存放計數。
-
無法處理循環引用 (如 a.b=b; b.a=a)。
-
頻繁更新引用計數降低了性能。
追蹤式回收算法 (Tracing)
追蹤式算法 (可達性分析) 的核心思想是判斷一個對象是否可達,如果這個對象一旦不可達就可以立刻被 GC 回收了,那麼我們怎麼判斷一個對象是否可達呢?
第一步從根節點開始找出所有的全局變量和當前函數棧裏的變量,標記爲可達。第二步,從已經標記的數據開始,進一步標記它們可訪問的變量,以此類推,專業術語叫傳遞閉包。當追蹤結束時,沒有被打上標記的對象就被判定是不可觸達。
優點:
-
解決了循環引用的問題
-
佔用的空間少了
和引用計數法相比,有以下缺點:
-
無法立刻識別出垃圾對象,需要依賴 GC 線程
-
算法在標記時必須暫停整個程序,即 STW(stop the world),否則其他線程有可能會修改對象的狀態從而回收不該回收的對象
如何清理垃圾
標記清除算法 (Mark Sweep)
標記清除算法是最常見的垃圾收集算法,標記清除收集器是跟蹤式垃圾收集器,其執行過程可以分成標記 (Mark) 和清除 (Sweep) 兩個階段:
-
標記階段:暫停應用程序的執行,從根對象觸發查找並標記堆中所有存活的對象;
-
清除階段:遍歷堆中的全部對象,回收未被標記的垃圾對象並將回收的內存加入空閒鏈表,恢復應用程序的執行;
優點:
- 實現簡單。
缺點:
-
執行期間需要把整個程序完全暫停,不能異步的進行垃圾回收。
-
容易產生大量不連續的內存隨便,碎片太多可能會導致後續沒有足夠的連續內存分配給較大的對象,從而提前觸發新的一次垃圾收集動作。
標記複製算法
它把內存空間劃分爲兩個相等的區域,每次只使用其中一個區域。在垃圾收集時,遍歷當前使用的區域,把存活對象複製到另一個區域中,最後將當前使用的區域的可回收對象進行回收。
實現:
-
首先這個算法會把對分成兩塊,一塊是 From、一塊是 To
-
對象只會在 From 上生成,發生 GC 之後會找到所有的存活對象,然後將其複製到 To 區,然後整體回收 From 區。
優點:
-
不用進行大量垃圾對象的掃描:標記複製算法需要從
GC-root
對象出發,將可達的對象複製到另外一塊內存後直接清理當前這塊內存即可。 -
解決了內存碎片問題,防止分配大空間對象是提前 gc 的問題。
缺點:
-
複製成本問題:在可達對象佔用內存高的時候,複製成本會很高。
-
內存利用率低:相當於可利用的內存僅有一半。
標記壓縮算法
在標記可回收的對象後將所有存活的對象壓縮到內存的一端,使他們緊湊地排列在一起,然後對邊界以外的內存進行回收,回收後,已用和未用的內存都各自一邊。
優點:
-
避免了內存碎片化的問題。
-
適合老年代算法,老年代對象存活率高的情況下,標記整理算法由於不需要複製對象,效率更高。
缺點:
- 整理過程複雜:需要多次遍歷內存,導致 STW 時間比標記清除算法高。
設計原理
三色標記算法
爲了解決原始標記清除算法帶來的長時間 STW, Go 從 v1.5 版本實現了基於三色標記清除的併發垃圾收集器,在不暫停程序的情況下即可完成對象的可達性分析,三色標記算法將程序中的對象分成白色、黑色和灰色三類:
-
白色對象 - 潛在的垃圾,表示還未搜索到的對象,其內存可能會被垃圾收集器回收;
-
黑色對象 - 活躍的對象,表示搜索完成的對象,包括不存在任何引用外部指針的對象以及從根對象可達的對象
-
灰色對象 - 活躍的對象,表示正在搜索還未搜索完的對象,因爲存在指向白色對象的外部指針,垃圾收集器會掃描這些對象的子對象;
三色標記法屬於增量式 GC 算法,回收器首先將所有對象標記成白色,然後從 gc root 出發,逐步把所有可達的對象變成灰色再到黑色,最終所有的白色對象都是不可達對象。
具體實現:
-
初始時所有對象都是白色的
-
從
gc root
對象出發,掃描所有可達對象標記爲灰色,放入待處理隊列 -
從隊列取出一個灰色對象並標記爲黑色,將其引用對象標記爲灰色,放入隊列
-
重複上一步驟,直到灰色對象隊列爲空
-
此時剩下的所有白色對象都是垃圾對象
優點:
- 不需要 STW
缺點:
-
如果產生垃圾速度大於回收速度時,可能會導致程序中垃圾對象越來越多而無法及時收集
-
線程切換和上下文轉換的消耗會使得垃圾回收的總體成本上升,從而降低系統吞吐量
三色標記法存在併發性問題,
-
可能會出現野指針 (指向沒有合法地址的指針),從而造成嚴重的程序錯誤
-
漏標,錯誤的回收非垃圾對象
三色不變性
想要在併發或者增量的標記算法中保證正確性,我們需要達成一下兩種三色不變性中的任意一種。
-
強三色不變性——黑色對象不會指向白色對象,只會指向灰色對象或者黑色對象。
-
弱三色不變性——黑色對象指向的白色對象必須包含一條從灰色對象經由多個白色對象的可達路徑。
屏障技術
垃圾收集中的屏障技術更像是一個鉤子方法,它是在用戶程序讀取對象、創建新對象以及更新對象指針時執行的一段代碼,根據操作類型的不同,我們可以將它們分成讀屏障和寫屏障兩種,因爲讀屏障需要在讀操作中加入代碼片段,對用戶程序的性能影響很大,所以變成語言往往都會採用寫屏障保證三色不變性。
插入寫屏障
當一個對象引用另外一個對象時,將另外一個對象標記爲灰色,以此滿足強三色不變性,不會存在黑色對象引用白色對象。
刪除寫屏障
在灰色對象刪除對白色對象的引用時,將白色對象置爲灰色,其實就是快照保存舊的引用關係,這叫 STAB(snapshot-at-the-beginning), 以此滿足弱三色不變性。
混合寫屏障
v1.8 版本之前,運行時會使用插入寫屏障保證強三色不變性;
在 v1.8 中,組合插入寫屏障和刪除寫屏障構成了混合寫屏障,保證弱三色不變性;該寫屏障會將覆蓋的對象標記成灰色 (刪除寫屏障) 並在當前棧沒有掃描時將新對象也標記成灰色(插入寫屏障):
寫屏障會將被覆蓋的指針和新指針都標記成灰色,而所有新建的對象都會被直接標記成黑色。
執行週期
Go 語言的垃圾收集可以分成清除終止、標記、標記終止和清除四個不同階段:
-
清理終止階段
-
暫停程序,所有的處理器在這時會進入安全點 (safe point);
-
如果當前垃圾收集循環是強制觸發的,我們還需要處理還未清理的內存管理單元;
-
標記階段
-
將狀態切換至
_GCmark
、開啓寫屏障、用戶程序協助 (Mutator Assists
) 並將根對象入隊; -
恢復執行程序,標記進程和用於協助的用戶程序會開始併發標記內存中的對象,寫屏障會將被覆蓋的指針和新指針都標記成灰色,而所有新創建的對象都會被直接標記成黑色;
-
開始掃描根對象,包括所有
Goroutine
的棧、全局對象以及不在堆中的運行時數據結構,掃描Goroutine
棧期間會暫停當前處理器; -
依次處理灰色隊列中的對象,將對象標記成黑色並將它們指向的對象標記成灰色;
-
使用分佈式的終止算法檢查剩餘的工作,發現標記階段完成後進入標記終止階段;
-
標記終止階段
-
暫停程序、將狀態切換至
_GCmarktermination
並關閉輔助標記的用戶程序; -
清理處理器上的線程緩存;
-
清理階段
-
將狀態切換至
_GCoff
開始清理階段、初始化清理狀態並關閉寫屏障; -
恢復用戶程序,所有新創建的對象會標記成白色;
-
後臺併發清理所有的內存管理單元,當
Goroutine
申請新的內存管理單元時就會觸發清理;
GC 觸發時機
當滿足觸發垃圾收集的基本條件:允許垃圾收集、程序沒有崩潰並且沒有處於垃圾循環;
注:運行時會通過如下所示的runtime.gcTrigger.test
方法決定是否需要觸發垃圾收集,該方法會根據三種不同方式觸發進行不同的檢查。
func (t gcTrigger) test() bool {
if !memstats.enablegc || panicking != 0 || gcphase != _GCoff {
return false
}
switch t.kind {
case gcTriggerHeap:
return memstats.heap_live >= memstats.gc_trigger
case gcTriggerTime:
if gcpercent < 0 {
return false
}
lastgc := int64(atomic.Load64(&memstats.last_gc_nanotime))
return lastgc != 0 && t.now-lastgc > forcegcperiod
case gcTriggerCycle:
return int32(t.n-work.cycles) > 0
}
return true
}
-
超過內存大小闕值,分配內存時,當前已分配內存與上一次
GC
結束時存活對象的內存達到某個比例時就觸發GC
。(默認配置會在堆內存達到上一次垃圾收集的 2 倍時,觸發新一輪的垃圾收集,可以通過環境變量GOGC
調整,在默認情況下他的值爲 100,即增長 100% 的堆內存纔會觸發GC
);比如一次回收完畢後,內存的使用量爲 5M,那麼下次回收的機制則是內存分配達到 10M 的時候,也就是說,並不是內存分配越多,垃圾回收頻率越高。 -
如果一直達不到內存大小的闕值,
sysmon
檢測出一段時間內(由runtime.forcegcperiod
變量控制,默認爲 2 分鐘)沒有觸發過GC
,就會觸發新的 GC。 -
調用
runtime.GC()
強制觸發GC
GC 調優
減少堆內存的分配是最好的優化方式。比如合理重複利用對象;避免string
和byte[]
之間的轉化等,兩者發生轉換的時候,底層數據結構會進行復制,因此導致 gc 效率會變低,少量使用+
連接string
,Go 裏面string
是最基礎的類型,是一個只讀類型,針對他的每一個操作都會創建一個新的string
,如果是少量小文本拼接,用“+”
就好,如果是大量小文本拼接,用strings.Join
; 如果是大量大文本拼接,用bytes.Buffer
。
優化努力的方向:
-
儘可能保持最小的堆內存
-
最佳的 GC 頻率
-
保持每次垃圾收集的內存大小
-
最小化每次垃圾收集的 STW 和 Mark Assist 的持續時間
轉自:
https://juejin.cn/post/7111515970669117447
Go 開發大全
參與維護一個非常全面的 Go 開源技術資源庫。日常分享 Go, 雲原生、k8s、Docker 和微服務方面的技術文章和行業動態。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/a1gtHznuYeLVuNPVPYGCwg