GO的GC辣雞回收(一)

aaayi發表於2021-09-02

使用者程式通過記憶體分配器(Allocator)在堆上申請記憶體,而垃圾收集器(Collector)負責回收堆上的記憶體空間,記憶體分配器和垃圾收集器共同管理程式中的堆記憶體空間。

基本概念

垃圾分類

  • 語義垃圾:也就是記憶體洩漏,指的是從語法上可達的物件,也就是被其他物件引用的,但是從語義上來講是垃圾。這類垃圾,垃圾回收是不管的
  • 語法垃圾:從語法上是不可達的物件,也就是沒有物件引用了,這些垃圾是垃圾回收重點照顧的物件

垃圾回收演算法

常見的垃圾回收演算法:

  • 引用計數:

    某個物件的根引用計數變為0時,其所有節點均需被回收

  • 標記壓縮:

    存活物件移動到一起,有效解決記憶體碎片問題

  • 複製演算法:

    將所有正在使用的物件從From空間複製到To空間,堆利用率只有一半,也能解決記憶體碎片問題

  • 標記清除:

    標記垃圾物件,然後再清除。解決不了記憶體碎片問題,需要與能儘量避免記憶體碎片的記憶體分配器使用,如tcmalloc

標記清除

標記清除演算法是最常見的垃圾收集演算法,標記清除收集器是跟蹤式垃圾收集器,其執行過程可以分為標記、清除兩個階段:

  1. 標記階段:從根物件出發查詢並標記堆中所有存活的物件
  2. 清除階段:遍歷堆中的全部物件,回收未被標記的垃圾物件並將回收的記憶體加入空閒連結串列

傳統的標記清除演算法,垃圾收集器從垃圾收集的根物件出發,遞迴遍歷這些物件指向的子物件並將所有可達的物件標記成存活。標記結束後,垃圾收集器會依次遍歷堆中的物件並清除其中的垃圾,整個過程需要標記物件的存活狀態,所以使用者程式在垃圾收集的過程中不能執行,也就是常說的STW(Stop The World)

三色抽象

為了解決原始標記清除演算法帶來的長時間STW,大部分追蹤式垃圾收集器都會實現三色標記演算法的變種以縮短STW的時間
三色標記演算法將程式中的物件分成白色、黑色、灰色:

  • 白色物件:潛在的垃圾,也就是沒有被掃到的物件,其記憶體可能會被垃圾收集器回收
  • 黑色物件:活躍的物件,包括不存在任何引用外部指標的物件以及從根物件可達的物件
  • 灰色物件:活躍的物件,因為存在指向白色物件的外部指標,垃圾收集器會掃描這些物件的子物件

標記過程:
垃圾收集器開始工作的時候,不存在任何黑色物件,根物件會被標記成灰色,垃圾收集器只會從灰色物件集合中取出物件開始掃描,當灰色集合中不存在任何物件時,標記就會結束。

大致工作原理:

  1. 從灰色物件的集合中選擇一個灰色物件並將其標記成黑色
  2. 將黑色物件指向的所有物件都標記成灰色,保證該物件和被該物件引用的物件都不會被回收
  3. 重複上述兩個步驟直到不存在灰色物件

當三色標記結束後,應用程式的堆中就不存在任何灰色物件,我們只能看到黑色的存活物件以及白色的垃圾物件,垃圾收集器可以回收這些白色的垃圾。
缺陷:
由於使用者程式可能在標記執行的過程中修改物件的指標,所以三色標記清除演算法本身是不可以併發或者增量執行的,仍需要STW。如果本來不應該被回收的物件被回收了,這在記憶體管理中是非常嚴重的錯誤,這種錯誤稱為懸掛指標,也就是指標沒有指向特定型別的合法物件,影響了記憶體的安全性,想要併發或者增量標記物件就需要使用屏障技術。

屏障技術

記憶體屏障技術是一種屏障指令,可以讓CPU或編譯器在執行記憶體相關的操作時遵循特定的約束,目前多數的現代處理器都會亂序執行指令以最大化效能,但是該技術能夠保證記憶體操作的順序性,在記憶體屏障前執行的操作一定會先於記憶體屏障後執行的操作。想在併發或增量的標記演算法中保證正確性,需要達到兩種三色不變性的其中一種。

三色不變性:

  • 強三色不變性:黑色物件不會指向白色物件,只會指向灰色物件或者黑色物件
  • 弱三色不變性:黑色物件指向的白色物件必須包含一條從灰色物件經由多個白色物件的可達路徑

屏障技術分為讀屏障和寫屏障,但是由於讀屏障需要在讀操作中加入程式碼片段,所以對使用者程式的效能影響較大。解析一下go語言中使用的兩種寫屏障技術,插入寫屏障和刪除寫屏障。

插入寫屏障(DIJKSTRA)

writePointer(slot, ptr):
    shade(ptr)
    *slot = ptr

每當要執行*slot = ptr時,會先執行寫屏障通過shade函式嘗試改變指標顏色。如果ptr指標是白色,那麼會將該物件設定成灰色。

這張圖中可以看到,在一次正常的標記過程中,發生了使用者程式修改了指標引用(或者新插入了一個引用關係)的情況,如果我們採用 插入寫屏障 我們就需要將新指向的物件標為灰色,以此保證強三色不變性。(對於新指向的物件來說,屬於被插入一個引用,所以叫插入寫屏障)

在插入寫屏障中,我們的標記過程變成(關鍵第2步):

  1. 基本標記流程不變,我們快進到垃圾收集器將根物件指向A物件標記成黑色,並將A物件指向的物件B標記成灰色。
  2. 這時使用者程式修改A物件的指標,將原本A物件指向B物件的指標指向C物件,這個時候就會觸發寫屏障將C物件標記為灰色,避免被錯誤回收。
  3. 垃圾收集器依次遍歷程式中的其他灰色物件,將他們分別標記成黑色

刪除寫屏障(YUASA)

writePointer(slot, ptr)
    shade(*slot)
    *slot = ptr

刪除寫屏障會在老物件的引用被刪除的時候,將白色的老物件塗成灰色,這樣就可以保證弱三色不變性,老物件引用的下游物件一定可以被灰色物件引用。

使用刪除寫屏障技術的垃圾收集器和使用者程式交替執行的場景中的標記過程:

  1. 垃圾收集器將根物件指向A物件標記成黑色並將A物件指向的物件B標記成灰色
  2. 如果這時使用者程式將B指向C的指標刪除,那麼C觸發刪除寫屏障,由於C是白色,所以被塗成灰色
  3. 垃圾收集器依次遍歷程式中的其他灰色物件,將他們分別標記成黑色

第二步觸發刪除寫屏障的著色,因為刪除了B指向C的指標,所以C和D分別違反強三色不變性和弱三色不變性,著色後保證了三色不變性,避免懸掛指標。

簡單來說就是,在改變指標指向的時候,原來指向的那個物件是白色的話就要變成灰色,以此保證弱三色不變性。(對於老物件來說,引用關係被解除了,所以叫刪除寫屏障)

增量和併發

兩種策略優化垃圾收集器不會以為回收垃圾導致長時間STW:

  • 增量垃圾收集:增量得標記和清除垃圾,降低應用程式暫停的最長時間
  • 併發垃圾手機:利用多核的計算資源,在使用者程式執行時併發標記和清除垃圾

由於兩種方式都需要垃圾收集器與使用者程式交替執行,所以需要配合屏障技術。

增量收集器

增量收集器將原本時間較長的暫停時間切分成多個更小的GC時間片。增量收集器需要配合三色標記法和屏障技術一起使用。將GC過程分段執行,雖然拉長了總的垃圾回收時間,但是減少了程式STW的時間。不過寫屏障還是有些開銷的。

併發收集器

利用多核優勢,將GC過程與使用者程式並行執行(大部分情況下),也是要配合屏障技術。

References

  1. https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-garbage-collector/
  2. https://spin.atomicobject.com/2014/09/03/visualizing-garbage-collection-algorithms/

相關文章