何為GC?
GC:Garbage Collection(垃圾回收)
垃圾指內(nèi)存中不再使用的內(nèi)存區(qū)域,自動(dòng)發(fā)現(xiàn)與釋放這種內(nèi)存區(qū)域的過程就是垃圾回收。
常見的垃圾回收機(jī)制:引用計(jì)數(shù)(python)、標(biāo)記-清除(go)、分代收集(JAVA)。
引用計(jì)數(shù):對(duì)每個(gè)對(duì)象維護(hù)一個(gè)引用計(jì)數(shù),當(dāng)引用該對(duì)象的對(duì)象被摧毀時(shí),引用計(jì)數(shù)減一,引用計(jì)數(shù)為零時(shí)回收該對(duì)象。
標(biāo)記-清除:從根變量開始遍歷所有引用的對(duì)象,引用的對(duì)象標(biāo)記為“被引用”,沒有被標(biāo)記的進(jìn)行回收。
分代收集:按照對(duì)象生命周期長短劃分不同的代空間,生命周期長的放入老年代,而短的放入新生代,不同代有不同的回收算法和回收頻率。
為什么要有GC?
程序運(yùn)行過程中會(huì)申請大量的內(nèi)存空間,但內(nèi)存資源是有限的,而對(duì)于一些無用的內(nèi)存空間如果不及時(shí)清理的話會(huì)導(dǎo)致內(nèi)存使用殆盡(內(nèi)存溢出),導(dǎo)致程序崩潰,因此管理內(nèi)存是一件重要且繁雜的事情
而垃圾回收可以讓內(nèi)存重復(fù)使用,并且減輕開發(fā)者對(duì)內(nèi)存管理的負(fù)擔(dān),減少程序中的內(nèi)存問題。
Go垃圾回收發(fā)展史
| 版本 | 發(fā)布時(shí)間 | GC算法 | STW時(shí)間 | 重大更新 |
|---|---|---|---|---|
| V1.1 | 2013/5 | STW | 可能秒級(jí)別 | |
| V1.3 | 2014/6 | Mark和Sweep分離,Mark、STW、Sweep并發(fā) | 百ms級(jí)別 | |
| V1.4 | 2014/12 | runtime代碼基本都由C和少量匯編改為Go和少量匯編, 包括GC部分, 以此實(shí)現(xiàn)了準(zhǔn)確式GC,減少了堆大小, 同時(shí)對(duì)指針的寫入引入了write barrier, 為1.5鋪墊 | 百ms級(jí)別 | |
| V1.5 | 2015/8 | 三色標(biāo)記法,并發(fā)Mark,并發(fā)Sweep。非分代、非移動(dòng)、并發(fā)的收集器 | 10ms-40ms級(jí)別 | 重要更新版本,生產(chǎn)上GC基本不會(huì)成為問題 |
| V1.6 | 2016/2 | 1.5中一些與并發(fā)GC不協(xié)調(diào)的地方更改。集中式的GC協(xié)調(diào)協(xié)程,改為狀態(tài)機(jī)實(shí)現(xiàn) | 5-20ms | |
| V1.7 | 2016/8 | GC時(shí)棧收縮改為并發(fā),span中對(duì)象分配狀態(tài)由freelist改為bitmap | 1-3ms左右 | |
| V1.8 | 2017/2 | hybird write barrier,消除了STW中的重新掃描棧 | sub ms | Golang GC進(jìn)入Sub ms時(shí)代 |
golang gc 觸發(fā)條件
| GC調(diào)用方式 | 所在位置 | 代碼 |
|---|---|---|
| 定時(shí)調(diào)用 | runtime/proc.go:forcegchelper() | gcStart(gcTrigger{kind: gcTriggerTime, now: nanotime()}) |
| 分配內(nèi)存時(shí)調(diào)用 | runtime/malloc.go:mallocgc() | gcTrigger{kind: gcTriggerHeap} |
| 手動(dòng)調(diào)用 | runtime/mgc.go:GC() | gcStart(gcTrigger{kind: gcTriggerCycle, n: n + 1}) |
STW(stop the world)
STW的過程中,CPU不執(zhí)行用戶代碼,全部用于垃圾回收
Root對(duì)象
根對(duì)象是mutator不需要通過其他對(duì)象就可以直接訪問到的對(duì)象. 比如全局對(duì)象, 棧對(duì)象, 寄存器中的數(shù)據(jù)等. 通過Root對(duì)象, 可以追蹤到其他存活的對(duì)象.
可達(dá)性
即通過對(duì)Root對(duì)象能夠直接或者間接訪問到.

Mark(標(biāo)記)
GC 開始,從 root 開始一層層掃描,掃描過程中把能被觸達(dá)的 object 標(biāo)記出來,那么堆空間未被標(biāo)記的 object 就是垃圾了
Sweep(清除)
遍歷堆空間所有 object 對(duì)未標(biāo)記的 object 進(jìn)行清除,清除完成則表示 GC 完成。
golang gc 演變過程
Go 1.1 GC過程

Go 1.3 GC過程

Go 1.5 GC過程
引入三色并發(fā)標(biāo)記法,三色標(biāo)記法過程如下。
step1:就是只要是新創(chuàng)建的對(duì)象,默認(rèn)的顏色都是標(biāo)記為【白色】
step2:GC回收開始,然后從根節(jié)點(diǎn)開始遍歷所有對(duì)象,把遍歷到的對(duì)象從【白色】集合放入【灰色】集合
step3:遍歷【灰色】集合,將【灰色】對(duì)象引用的對(duì)象從【白色】集合放入【灰色】集合,之后將此【灰色】對(duì)象放入【黑色】集合
step4:重復(fù)第三步, 直到灰色中無任何對(duì)象
step5:回收所有的白色標(biāo)記表的對(duì)象. 也就是回收垃圾
三色法動(dòng)態(tài)圖


1. 正常情況下,寫操作就是正常的賦值。
2. GC 開始,開啟寫屏障等準(zhǔn)備工作。開啟寫屏障等準(zhǔn)備工作需要短暫的 STW。
3. Stack scan 階段,從全局空間和 goroutine ??臻g上收集變量。
4. Mark 階段,執(zhí)行上述的三色標(biāo)記法,直到?jīng)]有灰色對(duì)象。
5. Mark termination 階段,開啟 STW,回頭重新掃描 root 區(qū)域新變量,對(duì)他們進(jìn)行標(biāo)記。
6. Sweep 階段,關(guān)閉 STW 和 寫屏障,對(duì)白色對(duì)象進(jìn)行清除。
寫屏障(write barrier)
這里就需要了解一下寫屏障的概念。這也是golang1.8如何去除Mark termination的關(guān)鍵。寫屏障的目標(biāo)就是要保障約束: 黑色對(duì)象不會(huì)指向白色對(duì)象,如果被指向了就會(huì)出現(xiàn)被清除的白色對(duì)象,實(shí)際是被引用的對(duì)象,造成錯(cuò)誤的清理
寫屏障的實(shí)現(xiàn)有很多模式,在golang1.7之前主要采用的是Dijkstra-style insertion write barrier,其偽碼實(shí)現(xiàn)如下:
writePointer(slot, ptr):
shade(ptr)
*slot = ptr
其思路就是在進(jìn)行指針的重定向時(shí),將被指向的指針對(duì)象標(biāo)記為灰色(shade it),這樣如果有新的對(duì)象被創(chuàng)建或者黑色對(duì)象指向白色對(duì)象時(shí),目標(biāo)對(duì)象就會(huì)標(biāo)灰,從而滿足了黑色對(duì)象不會(huì)指向白色對(duì)象的約束。
“強(qiáng)-弱” 三色不變式
- 強(qiáng)三色不變式
不存在黑色對(duì)象引用到白色對(duì)象的指針

- 弱三色不變式
所有被黑色對(duì)象引用的白色對(duì)象都處于灰色保護(hù)狀態(tài)

為了遵循上述的兩個(gè)方式,Golang團(tuán)隊(duì)初步得到了如下具體的兩種屏障方式“插入屏障”, “刪除屏障”。
Go 1.8 GC過程
三色標(biāo)記方式,需要在最后重新掃描一下所有全局變量和 goroutine ??臻g,如果系統(tǒng)的 goroutine 很多,這個(gè)階段耗時(shí)也會(huì)比較長,甚至?xí)L達(dá) 100ms。畢竟 Goroutine 很輕量,大型系統(tǒng)中,上百萬的 Goroutine 也是常有的事兒。
Go 在1.8 版本使用混合寫屏障(hybrid write barrier)機(jī)制,將第一次短暫的 STW取消了,在1.9 版本又大大減少了第二次的STW。
大致如下圖所示:
GC開始,默認(rèn)都是白色

掃描棧區(qū),將可達(dá)對(duì)象全部標(biāo)記為黑

Golang中的混合寫屏障滿足弱三色不變式,結(jié)合了刪除寫屏障和插入寫屏障的優(yōu)點(diǎn),只需要在開始時(shí)并發(fā)掃描各個(gè)goroutine的棧,使其變黑并一直保持,這個(gè)過程不需要STW,而標(biāo)記結(jié)束后,因?yàn)闂T趻呙韬笫冀K是黑色的,也無需再進(jìn)行re-scan操作了,減少了STW的時(shí)間