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開始前各對象的狀態如下圖所示:
- 初始狀態下所有對象都是白色的。
- 接着開始掃描根對象a、b; 由於根對象引用了對象A、B,那么A、B變為灰色對象,接下來就開始分析灰色對象,分析A時,A沒有引用其他對象很快就轉入黑色,B引用了D,則B轉入黑色的同時還需要將D轉為灰色,進行接下來的分析。
- 灰色對象只有D,由於D沒有引用其他對象,所以D轉入黑色。標記過程結束
- 最終,黑色的對象會被保留下來,白色對象會被回收掉。
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流程
- Sweep Termination: 對未清掃的span進行清掃, 只有上一輪的GC的清掃工作完成才可以開始新一輪的GC
- Mark: 掃描所有根對象, 和根對象可以到達的所有對象, 標記它們不被回收
- Mark Termination: 完成標記工作, 重新掃描部分根對象(要求STW)
- 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的指針。
- GC先掃描A,A放入黑色
- B把C的指針交給A
- GC再掃描B,B放入黑色
- C在白色,會回收;但是A其實引用了C。
為了避免這個問題, go在GC的標記階段會啟用寫屏障(Write Barrier).
啟用了寫屏障(Write Barrier)后,
- GC先掃描A,A放入黑色
- B把C的指針交給A
- 由於A在黑色,所以C放入灰色
- C沒有子對象,放入黑色
- 掃描B,B沒有子對象,放入黑色
即使A可能會在稍后丟掉C, 那么C就在下一輪回收。
開啟寫屏障之后,當指針發生改變, GC會認為在這一輪的掃描中這個指針是存活的, 所以放入灰色。