1. 程式人生 > 實用技巧 >Golang GC(垃圾回收機制)

Golang GC(垃圾回收機制)

目錄

Golang GC

1.常見的垃圾回收機制

1.1 引用計數

對每個物件維護一個引用計數,當引用物件的物件被銷燬時,飲用計數-1,如果引用計數為0,則進行垃圾回收

  • 優點:物件可以很快的被回收,不會出現記憶體耗盡或達到某個閥值時才回收。
  • 缺點:不能很好的處理迴圈引用,而且實時維護引用計數,有也一定的代價。
  • 代表語言:Python、PHP、Swift

1.2 標記-清除

從根變數開始遍歷所有引用的物件,引用的物件標記為"被引用",沒有被標記的進行回收。

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

1.3 分代收集

按照物件生命週期長短劃分不同的代空間,生命週期長的放入老年代,而短的放入新生代,不同代有不能的回收演算法和回收頻率。

  • 優點:回收效能好
  • 缺點:演算法複雜
  • 代表語言: JAVA

2. Golang的標記清除

如下圖所示,通過gcmarkBits點陣圖標記span的塊是否被引用。對應記憶體分配中的bitmap區。

2.1 三色標記

  • 灰色:物件已被標記,但這個物件包含的子物件未標記
  • 黑色:物件已被標記,且這個物件包含的子物件也已標記,gcmarkBits對應的位為1(該物件不會在本次GC中被清理)
  • 白色:物件未被標記,gcmarkBits對應的位為0(該物件將會在本次GC中被清理)

例如,當前記憶體中有A~F一共6個物件,根物件a,b本身為棧上分配的區域性變數,根物件a、b分別引用了物件A、B, 而B物件又引用了物件D,則GC開始前各物件的狀態如下圖所示:

  1. 初始狀態下所有物件都是白色的。
  2. 接著開始掃描根物件a、b; 由於根物件引用了物件A、B,那麼A、B變為灰色物件,接下來就開始分析灰色物件,分析A時,A沒有引用其他物件很快就轉入黑色,B引用了D,則B轉入黑色的同時還需要將D轉為灰色,進行接下來的分析。
  3. 灰色物件只有D,由於D沒有引用其他物件,所以D轉入黑色。標記過程結束
  4. 最終,黑色的物件會被保留下來,白色物件會被回收掉。

2.2 GC的觸發

  • 閾值:預設記憶體擴大一倍,啟動gc
  • 定期:預設2min觸發一次gc,src/runtime/proc.go:forcegcperiod
  • 手動:runtime.gc()

2.3 STW

stop the world是gc的最大效能問題,對於gc而言,需要停止所有的記憶體變化,即停止所有的goroutine,等待gc結束之後才恢復。

標記-清除(mark and sweep)演算法的STW(stop the world)操作,就是runtime把所有的執行緒全部凍結掉,所有的執行緒全部凍結意味著使用者邏輯是暫停的。這樣所有的物件都不會被修改了,這時候去掃描是絕對安全的。

Go如何減短這個過程呢?標記-清除(mark and sweep)演算法包含兩部分邏輯:標記和清除。

我們知道Golang三色標記法中最後只剩下的黑白兩種物件,黑色物件是程式恢復後接著使用的物件,如果不碰觸黑色物件,只清除白色的物件,肯定不會影響程式邏輯。所以: 清除操作和使用者邏輯可以併發。

標記操作和使用者邏輯也是併發的,使用者邏輯會時常生成物件或者改變物件的引用,那麼標記和使用者邏輯如何併發呢?這裡就讓說到golang的寫屏障了,我們在2.5中介紹。

2.4 GC流程

  1. Sweep Termination: 對未清掃的span進行清掃, 只有上一輪的GC的清掃工作完成才可以開始新一輪的GC
  2. Mark: 掃描所有根物件, 和根物件可以到達的所有物件, 標記它們不被回收
  3. Mark Termination: 完成標記工作, 重新掃描部分根物件(要求STW)
  4. Sweep: 按標記結果清掃span

目前整個GC流程會進行兩次STW(Stop The World), 第一次是Mark階段的開始, 第二次是Mark Termination階段.

  • 第一次STW會準備根物件的掃描, 啟動寫屏障(Write Barrier)和輔助GC(mutator assist).
  • 第二次STW會重新掃描部分根物件, 禁用寫屏障(Write Barrier)和輔助GC(mutator assist).

需要注意的是, 不是所有根物件的掃描都需要STW, 例如掃描棧上的物件只需要停止擁有該棧的G.
從go 1.9開始, 寫屏障的實現使用了Hybrid Write Barrier, 大幅減少了第二次STW的時間.

2.5 寫屏障

因為go支援並行GC, GC的掃描和go程式碼可以同時執行,這樣帶來的問題是GC掃描的過程中go程式碼有可能改變了物件的依賴樹。

例如開始掃描時發現根物件A和B,B擁有C的指標。

  1. GC先掃描A,A放入黑色
  2. B把C的指標交給A
  3. GC再掃描B,B放入黑色
  4. C在白色,會回收;但是A其實引用了C。

為了避免這個問題, go在GC的標記階段會啟用寫屏障(Write Barrier).

啟用了寫屏障(Write Barrier)後,

  1. GC先掃描A,A放入黑色
  2. B把C的指標交給A
  3. 由於A在黑色,所以C放入灰色
  4. C沒有子物件,放入黑色
  5. 掃描B,B沒有子物件,放入黑色

即使A可能會在稍後丟掉C, 那麼C就在下一輪迴收。

開啟寫屏障之後,當指標發生改變, GC會認為在這一輪的掃描中這個指標是存活的, 所以放入灰色