Go的三色標記GC
- 引用計數(shù):對每個對象維護一個引用計數(shù),當引用該對象的對象被銷毀時,引用計數(shù)減1,當引用計數(shù)器為0是回收該對象。
- 優(yōu)點:對象可以很快的被回收,不會出現(xiàn)內(nèi)存耗盡或達到某個閥值時才回收。
- 缺點:不能很好的處理循環(huán)引用,而且實時維護引用計數(shù),有也一定的代價。
- 代表語言:Python、php、Swift
- 標記-清除:從根變量開始遍歷所有引用的對象,引用的對象標記為"被引用",沒有被標記的進行回收。
- 優(yōu)點:解決了引用計數(shù)的缺點。
- 缺點:需要STW,即要暫時停掉程序運行。
- 代表語言:Golang(其采用三色標記法)
- 分代收集:按照對象生命周期長短劃分不同的代空間,生命周期長的放入老年代,而短的放入新生代,不同代有不能的回收算法和回收頻率。
- 優(yōu)點:回收性能好
- 缺點:算法復(fù)雜
- 代表語言: JAVA,python
root
首先標記root根對象,根對象的子對象也是存活的。
根對象包括:全局變量,各個G stack上的變量等。
標記
在之前的Go語言——內(nèi)存管理一文中,分析過span是內(nèi)存管理的最小單位,所以猜測gc的粒度也是span。
type mspan struct {
// allocBits and gcmarkBits hold pointers to a span's mark and
// allocation bits. The pointers are 8 byte aligned.
// There are three arenas where this data is held.
// free: Dirty arenas that are no longer accessed
// and can be reused.
// next: Holds information to be used in the next GC cycle.
// current: Information being used during this GC cycle.
// previous: Information being used during the last GC cycle.
// A new GC cycle starts with the call to finishsweep_m.
// finishsweep_m moves the previous arena to the free arena,
// the current arena to the previous arena, and
// the next arena to the current arena.
// The next arena is populated as the spans request
// memory to hold gcmarkBits for the next GC cycle as well
// as allocBits for newly allocated spans.
//
// The pointer arithmetic is done "by hand" instead of using
// arrays to avoid bounds checks along critical performance
// paths.
// The sweep will free the old allocBits and set allocBits to the
// gcmarkBits. The gcmarkBits are replaced with a fresh zeroed
// out memory.
allocBits *gcBits
gcmarkBits *gcBits
}

bitmap
如圖所示,通過gcmarkBits位圖標記span的塊是否被引用。對應(yīng)內(nèi)存分配中的bitmap區(qū)。
三色標記
- 灰色:對象已被標記,但這個對象包含的子對象未標記
- 黑色:對象已被標記,且這個對象包含的子對象也已標記,gcmarkBits對應(yīng)的位為1(該對象不會在本次GC中被清理)
- 白色:對象未被標記,gcmarkBits對應(yīng)的位為0(該對象將會在本次GC中被清理)
例如,當前內(nèi)存中有A~F一共6個對象,根對象a,b本身為棧上分配的局部變量,根對象a、b分別引用了對象A、B, 而B對象又引用了對象D,則GC開始前各對象的狀態(tài)如下圖所示:
- 初始狀態(tài)下所有對象都是白色的。
- 接著開始掃描根對象a、b; 由于根對象引用了對象A、B,那么A、B變?yōu)榛疑珜ο螅酉聛砭烷_始分析灰色對象,分析A時,A沒有引用其他對象很快就轉(zhuǎn)入黑色,B引用了D,則B轉(zhuǎn)入黑色的同時還需要將D轉(zhuǎn)為灰色,進行接下來的分析。
- 灰色對象只有D,由于D沒有引用其他對象,所以D轉(zhuǎn)入黑色。標記過程結(jié)束
- 最終,黑色的對象會被保留下來,白色對象會被回收掉。

STW
stop the world是gc的最大性能問題,對于gc而言,需要停止所有的內(nèi)存變化,即停止所有的goroutine,等待gc結(jié)束之后才恢復(fù)。
觸發(fā)
- 閾值:默認內(nèi)存擴大一倍,啟動gc
- 定期:默認2min觸發(fā)一次gc,src/runtime/proc.go:forcegcperiod
- 手動:runtime.gc()
go gc

go gc
GO的GC是并行GC, 也就是GC的大部分處理和普通的go代碼是同時運行的, 這讓GO的GC流程比較復(fù)雜.
- Stack scan:Collect pointers from globals and goroutine stacks。收集根對象(全局變量,和G stack),開啟寫屏障。全局變量、開啟寫屏障需要STW,G stack只需要停止該G就好,時間比較少。
- Mark: Mark objects and follow pointers。標記所有根對象, 和根對象可以到達的所有對象不被回收。
- Mark Termination: Rescan globals/changed stack, finish mark。重新掃描全局變量,和上一輪改變的stack(寫屏障),完成標記工作。這個過程需要STW。
- Sweep: 按標記結(jié)果清掃span
目前整個GC流程會進行兩次STW(Stop The World), 第一次是Stack scan階段, 第二次是Mark Termination階段.
- 第一次STW會準備根對象的掃描, 啟動寫屏障(Write Barrier)和輔助GC(mutator assist).
- 第二次STW會重新掃描部分根對象, 禁用寫屏障(Write Barrier)和輔助GC(mutator assist).
從1.8以后的golang將第一步的stop the world 也取消了,這又是一次優(yōu)化; 1.9開始, 寫屏障的實現(xiàn)使用了Hybrid Write Barrier, 大幅減少了第二次STW的時間.
寫屏障
因為go支持并行GC, GC的掃描和go代碼可以同時運行, 這樣帶來的問題是GC掃描的過程中g(shù)o代碼有可能改變了對象的依賴樹。
例如開始掃描時發(fā)現(xiàn)根對象A和B, B擁有C的指針。
- GC先掃描A,A放入黑色
- B把C的指針交給A
- GC再掃描B,B放入黑色
- C在白色,會回收;但是A其實引用了C。
為了避免這個問題, go在GC的標記階段會啟用寫屏障(Write Barrier).
啟用了寫屏障(Write Barrier)后,在GC第三輪rescan階段,根據(jù)寫屏障標記將C放入灰色,防止C丟失。
參考:
Go 垃圾回收原理
Golang源碼探索(三) GC的實現(xiàn)原理