天天看點

golang gc垃圾回收機制

作者:幹飯人小羽

1.常見的垃圾回收機制#

1.1 引用計數#

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

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

1.2 标記-清除#

從根變量開始周遊所有引用的對象,引用的對象标記為"被引用",沒有被标記的進行回收。

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

1.3 分代收集#

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

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

2. Golang的标記清除#

如下圖所示,通過gcmarkBits位圖示記span的塊是否被引用。對應記憶體配置設定中的bitmap區。

golang gc垃圾回收機制

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. 最終,黑色的對象會被保留下來,白色對象會被回收掉。
golang gc垃圾回收機制

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會認為在這一輪的掃描中這個指針是存活的, 是以放入灰色