對于學習Java的開發來說, GC并不陌生, 實際上Go的GC流程與Java的CMS實現上不盡相同, 但是流程基本類似. 而對于公司大部分C/C++的開發者來說, 習慣了盡量使用棧對象, 手動管理內存,盡量少new, 對GC的一些術語, 流程可能就有點陌生了, 或許可能對GC有一些些懷疑(實際上20世紀90年代后誕生的, 得到廣泛應用的語言, 只有VB沒有自動內存管理).
如果從手動管理內存, 跨越到GC, 是我的話, 我可能會有以下疑問:
有GC的語言是咋樣的? (簡化內存管理)
GC可以幫我釋放文件嗎? (可以, 但不建議. finalization)
GC會不會出現泄漏?(正確性問題)
GC多久一次?(GC頻率)
GC會不會暫停很久?(Stop The World問題)
每次GC的暫停穩不穩定?(STW時間分布)
GC會不會拖累我程序的運行速度?(吞吐量問題)
GC是如何與程序并發運行的?(并發標記)
會不會導致碎片很多?(壓縮和碎片)
GC會不會一直不回收, 撐爆內存? (GC觸發)
GC影響程序, 如何調優?
Go是GC是怎么實現的?
Go的GC與其他語言對比如何?
Golang GC系列共分為三篇. 分別為:
Golang GC的過去, 當前與未來
GC基礎理論與基礎算法與Golang GC流程
Golang GC源碼解析
本文主分為三大部分.第一部分主要講Golang GC每個版本的變更歷史, 重要版本更新.
第二部分是官方給出的一些GC STW時間.
第三部分是本人在實際服務和模擬測試中的一些結果.
最后給出一些重要的proposal和desgin docs.
閱讀完本文, 你會對GC有大致了解, Go GC發展歷程有大致了解, 對Go GC的流程有大致了解, 對Go GC性能有大致了解, 排除對GC的恐懼. 在后續如果要考慮性能問題時, 有數據可參考, 心中有數.
至于GC基礎算法, Golang GC具體流程, 以及源碼解析, 將在后續幾篇闡述.
本文基于Golang 1.11 Linux amd64.
[TOC]
GC相關概念
GC簡介
垃圾回收(英語:Garbage Collection,縮寫為GC)是一種自動內存管理機制. 垃圾回收器(Garbage Collector)嘗試回收不再被程序所需要的對象所占用的內存. GC最早起源于LISP語言, 1959年左右由John McCarthy創造用以簡化LISP中的內存管理. 所以GC可以說是一項"古老"的技術, 但是直到20世紀90年代Java的出現并流行, 廣大的普通程序員們才得以接觸GC. 當前許多語言如Go, Java, C#, JS和Python等都支持GC.
手動管理內存 VS 自動管理內存
存活是一個全局的(global)特征, 但是調用free函數將對象釋放卻是局部行為
手動內存管理需要開發者時刻注意對象的生命周期.
顯式的指定哪些對象要釋放并歸還給操作系統
同時還需要注意需要清空指向已經釋放對象的指針
注意不能過早的回收還在引用的對象
在處理有循環引用的對象或者指針操作非線程安全的情況下, 非常的復雜.
調用其他方法或者第三方庫時, 需要明確對象所有權, 需要明確其對象管理方式,加大了耦合性.
GC可以解決大部分懸掛指針和內存泄漏的問題.
GC可以將未被任何對象引用的對象的進行回收, 從而避免懸掛指針.
只有回收器可以釋放對象, 所以不會出現二次釋放(double-freeing)
回收器掌握堆中對象的全局信息以及所有可能訪問堆中對象的線程信息, 因此其可以決定任意對象是否需要回收.
回收器管理對象, 模塊之間減少了耦合.
以下為不帶GC的C++與有GC的Golang的簡單對比.
C++
for(int i = 0; i < 100; i++)
{
Person * p = new Person();
doSomethingWithP(p);
....
//時刻需要注意, 分配了就要釋放
delete p;
}
//錯誤
int* returnValue()
{
int arr[]={1,2,3,4,5};
return arr;
}
Golang
for i:=0;i<100 ;i++ {
o:=new(Person)
......
//隨便怎么用, 不需要顯示釋放
}
//完全OK
func ReturnValue() *Person {
arr:=[]int{1,2,3,4,5}
return arr
}
GC大大減少了開發者編碼時的心智負擔, 把精力集中在更本質的編程工作上, 同時也大大減少了程序的錯誤.
GC與資源回收
GC是一種內存管理機制. 在有GC的語言中也要注意釋放文件, 連接, 數據庫等資源!
前面提到GC是一種自動內存管理機制, 回收不再使用的對象的內存. 除了內存外, 進程還占用了socket, 文件描述符等資源等, 一般來說這些都不是GC處理的.
有一些GC系統可以將一些系統資源與一塊內存關聯, 在回收內存時, 其相關的資源也被釋放, 這種機制稱為finalization. 但是finalization存在很大的不足, 因為GC是不確定的, 無法明確GC什么時候會發生, finalization無法像析構函數那樣精確的控制系統資源的釋放, 資源的不再使用與被釋放之間可能存在很大的時延, 也無法控制由誰釋放資源.
Java和Go均有類似的機制, 目前Java 1.9中已經明確把finalizer標記為廢棄.
術語簡單說明
這里簡單的說明一些術語,幫助快速了解, 并不追求完全準確.
mutator
mutate的是變化的意思, mutator就是改變者, 在GC里, 指的是改變對象之間引用關系的實體, 可以簡單的理解為我們寫的的應用程序(運行我們寫的代碼的線程, 協程).
allocator, collector
自動內存管理機制一般包含allocator(分配器)和collector(回收器). allocator負責為應用代碼分配對象, 而collector則負責尋找存活的對象, 并釋放不再存活的對象.
STW
stop the world, GC的一些階段需要停止所有的mutator(應用代碼)以確定當前的引用關系. 這便是很多人對GC擔心的來源, 這也是GC算法優化的重點. 對于大多數API/RPC服務, 10-20ms左右的STW完全接受的. Golang GC的STW時間從最初的秒級到百ms, 10ms級別, ms級別, 到現在的ms以下, 已經達到了準實時的程度.
Root對象
根對象是mutator不需要通過其他對象就可以直接訪問到的對象. 比如全局對象, 棧對象, 寄存器中的數據等. 通過Root對象, 可以追蹤到其他存活的對象.
可達性
即通過對Root對象能夠直接或者間接訪問到.
對象的存活
如果某一個對象在程序的后續執行中可能會被mutator訪問, 則稱該對象是存活的, 不存活的對象就是我們所說的garbage. 一般通過可達性來表示存活性.
Mark Sweep
三大GC基礎算法中的一種. 分為mark(標記)和sweep(清掃)兩個階段. 樸素的Mark Sweep流程如下:
Stop the World
Mark: 通過Root和Root直接間接訪問到的對象, 來尋找所有可達的對象, 并進行標記
Sweep: 對堆對象迭代, 已標記的對象置位標記. 所有未標記的對象加入freelist, 可用于再分配.
Start the Wrold
樸素的Mark Sweep是整體STW, 并且分配速度慢, 內存碎片率高.
有很多對Mark Sweep的優化,
比如相同大小階梯的對象分配在同一小塊內存中, 減少碎片率.
freelist改成多條, 同一個大小范圍的對象, 放在一個freelist上, 加快分配速率, 減少碎片率.
并發Sweep和并發Mark, 大大降低stw時間.
并發收集:
樸素的Mark Sweep算法會造成巨大的STW時間, 導致應用長時間不可用, 且與堆大小成正比, 可擴展性不好. Go的GC算法就是基于Mark Sweep, 不過是并發Mark和并發Sweep.
一般說并發GC有兩層含義, 一層是每個mark或sweep本身是多個線程(協程)執行的(concurrent),一層是mutator(應用程序)和collector同時運行(background).
首先concurrent這一層是比較好實現的, GC時整體進行STW, 那么對象引用關系不會再改變, 對mark或者sweep任務進行分塊, 就能多個線程(協程)conncurrent執行任務mark或sweep.
而對于backgroud這一層, 也就是說mutator和mark, sweep同時運行, 則相對復雜.
首先backgroup sweep是比較容易實現的, 因為mark后, 哪些對象是存活, 哪些是要被sweep是已知的, sweep的是不再引用的對象, sweep結束前, 這些對象不會再被分配到. 所以sweep容和mutator內存共存, 后面我們可以看到golang是先在1.3實現的sweep并發. 1.5才實現的mark并發.
寫屏障
接上面, mark和mutator同時運行就比較麻煩, 因為mutator會改變已被scan的對象的引用關系.
假設下面這種情況:
mutator和collector同時運行.
b有c的引用. gc開始, 先掃描了a, 然后mutator運行, a引用了c, b不再引用c, gc再掃描b, 然后sweep, 清除了c. 這里其實a還引用了c, 導致了正確性問題.
b.obj1=c
gc mark start
gc scan a
mutaotr a.obj1=c
mutator b.obj1=nil
gc scan b
gc mark termination
sweep and free c(error)
為了解決這個問題, go引入了寫屏障(寫屏障有多種類型, Dijkstra-style insertion write barrier, Yuasa-style deletion write barrier等). 寫屏障是在寫入指針前執行的一小段代碼用于防止指針丟失. 這一小段代碼Golang是在編譯時寫入的. Golang目前寫屏障在mark階段開啟.
Dijkstra write barrier在
mutaotr a.obj1=c
這一步, 將c的指針寫入到a.obj1之前, 會先執行一段判斷代碼, 如果c已經被掃描過, 就不再掃描, 如果c沒有被掃描過, 就把c加入到待掃描的隊列中. 這樣就不會出現丟失存活對象的問題存在.
三色標記法
三色標記法是傳統Mark-Sweep的一個改進, 由Dijkstra(就是提出最短路徑算法的)在1978年發表的論文On-the-Fly Garbage Collection: An Exercise in Cooperation中提出.
它是一個并發的GC算法.
原理如下,
首先創建三個集合:白, 灰, 黑. 白色節點表示未被mark和scan的對象, 灰色節點表示已經被mark, 但是還沒有scan的對象, 而黑色表示已經mark和scan完的對象.
初始時所有對象都在白色集合.
從根節點開始廣度遍歷, 將其引用的對象加入灰色集合.
遍歷灰色集合, 將灰色對象引用的白色對象放入灰色集合, 之后將此灰色對象放入黑色集合.
標記過程中通過write-barrier檢測對象引用的變化.重復4直到灰色中無任何對象. GC結束, 黑色對象為存活對象, 而剩下的白色對象就是Garbage. Sweep所有白色對象.
下面我們會提到Golang也是使用的三色標記法. 在Go Runtime的實現中, 并沒有白色集合, 灰色集合, 黑色集合這樣的容器. 實現如下:
白色對象: 某個對象對應的gcMarkBit為0(未被標記)
灰色對象: gcMarkBit為1(已被標記)且在(待scan)gcWork的待scan buffer中
黑色對象: gcMarkBit為1(已被標記)且不在(已經scan)gcWork的待scan buffer中
Golang GC發展歷史
Golang GC簡介
Golang對于GC的目標是低延遲, 軟實時GC, 很多圍繞著這兩點來設計.
Golang剛發布時(13,14年)GC飽受詬病, 相對于當時Java成熟的CMS(2002年JDK1.4中發布)和G1(2012年JDK7中發布)來說, 不管在吞吐量還是暫停時間控制上來說, 都有比較大的差距.
1.3-1.9之間的Golang版本更新, 都把GC放在了重要的改進點上, 從最初的STW算法到1.5的三色并發標記, 再到1.8的hybird write barrier, 完全消除了并發標記-清除算法需要的重新掃描棧階段, Golang GC做到了sub ms的gc pause.
目前Golang GC(我這里指Go 1.11, 2018年8月發布)具有以下特征.
三色標記
Mark Sweep算法,并發標記, 并發清除
Muator會執行輔助標記, 輔助清掃
非分代
準確式GC, 能夠知道內存中某個數據是數字還是指向對象的指針. 相對的是保守式GC.
非緊縮, 非移動,GC之后不會進行緊縮堆(也就不會移動堆對象地址)
寫屏障實現增量式
在生產上, Golang GC對GC Pause的控制在Go 1.6以后超過了Java CMS和G1.當然Java 11(2018年9月發布)中加入了實驗性質的ZGC, 在128G的堆上, GC Pause能夠100%控制在2ms內, 是非常厲害的. 目前可能Java用的比較多的還是JDK7或者JDK8, 那么GC一般是CMS和少量的G1, 而Go的版本一般都在1.9以后, 這一點上, 生產上Go的GC體驗還是比Java好一些.
Go1.6中的gc pause已經完全超越JVM了嗎?
https://www.zhihu.com/question/42353634 R大的回答
Golang各版本GC改進簡介
以下是簡單介紹Golang GC的版本更新以及STW時間(以下STW時間僅供參考, 因為STW時間與機器性能, 堆大小, 對象數量, 應用分配偏好, 都有很大的關系)
版本 | 發布時間 | GC算法 | STW時間 | 重大更新 |
---|---|---|---|---|
V1.1 | 2013/5 | STW | 可能秒級別 | |
V1.3 | 2014/6 | Mark和Sweep分離. Mark STW, Sweep并發 | 百ms級別 | |
V1.4 | 2014/12 | runtime代碼基本都由C和少量匯編改為Go和少量匯編, 包括GC部分, 以此實現了準確式GC,減少了堆大小, 同時對指針的寫入引入了write barrier, 為1.5鋪墊 | 百ms級別 | |
V1.5 | 2015/8 | 三色標記法, 并發Mark, 并發Sweep. 非分代, 非移動, 并發的收集器 | 10ms-40ms級別 | 重要更新版本,生產上GC基本不會成為問題 |
V1.6 | 2016/2 | 1.5中一些與并發GC不協調的地方更改. 集中式的GC協調協程, 改為狀態機實現 | 5-20ms | |
V1.7 | 2016/8 | GC時棧收縮改為并發, span中對象分配狀態由freelist改為bitmap | 1-3ms左右 | |
V1.8 | 2017/2 | hybird write barrier, 消除了stw中的重新掃描棧 | sub ms | Golang GC進入Sub ms時代 |
Golang 1.12(2019.2月發布)中通過對Mark Termination的一些工作階段清理, 基本消除了這個階段的工作, 據測試, stw相對于1.11又降低了接近一半.
GC's STW pauses in Go1.12 beta are much shorter than Go1.11
https://www.reddit.com/r/golang/comments/aetba6/gcs_stw_pauses_in_go112_beta_are_much_shorter/
Golang GC官方數據
因為本人并沒有經歷過golang 1.1-1.8的時代, 也沒去測試過以前版本的具體數據. 以下GC的STW時間從網絡上文章及官方分享中獲取, 數據供參考, 但不會出現數量級的問題.
2015年Go官方對golang 1.5 GC闡述的一個ppt
- Go1.3-Go1.5
https://talks.golang.org/2015/go-gc.pdf
以下數據來自前google和前twitter工程師Brain Hatfield, 每隔半年在twitter上發表的一個服務升級Golang版本帶來的GC提升. 這些數據在Golang GC掌門人Richard L. Hudson的分享上也列舉過. 大概10GB級別堆從1.4-1.8的STW數據對比: https://twitter.com/brianhatfield/status/804355831080751104
Go 1.4-Go 1.5
2015/8月重新編譯發布
不清楚堆大小, STW時間由1.4的300-400ms下降到20-50ms.
Go1.5-Go 1.6
2016年1月編譯發布
應該與前面是同一個服務, STW由1.5的20ms左右降為3-5ms.
Go 1.6-Go 1.7
2016年8月編譯發布, Go1.6升級1.7后, 服務stw時間由3ms降為1-2ms
Go 1.7-Go 1.8
2016年12月編譯發布, 升級后, 服務STW時間由2-3ms->1ms以下
Go 1.9
2017年8月
此圖為go 1.9在18GB堆下, stw時間, 都在1.0ms以下. go1.8版本之后, stw時間的提升均不大, 已經到sub ms了.
[圖片上傳失敗...(image-91ad18-1555253764980)]
總結圖
Golang 1.4-1.9 STW時間與版本關系.
當前生產中的Golang GC
Golang 1.8后GC的STW時間基本上做到了和堆大小無關. 而并發Mark時間則與存活對象數目(當然這個描述并不是非常準確)基本成正比, 與CPU的核數基本成反比.
存活對象較少的堆
對于我們的業務系統來說, 一般都是有大量的臨時對象. 反而總的存活對象不會很多. 我們先來看看堆里面存活對象不多的服務的GC情況.
測試方式
以下來自某服務.
單臺機器8核,16G虛擬機, CPU利用率為50%時, 單機服務請求量6W/min, 1000qps/s, 該進程占用370% CPU.
kv存儲調用 18W/min, 3000qps/s
rpc調用 24W/min, 4000qps/s
client調用(合并rpc和kv) 43W/min,7000qps/s
遠程日志數據CGO傳遞給C++ Agent的共享內存, 3W/s.
(服務的qps倒不是很高, 主要是這個服務每次都會發6個rpc請求, 且沒有優化過, 火焰圖里可以看出來日志里序列化pb, 耗費了30%的性能)
結果與分析
可以看出STW時間基本都在1ms以下, 有小部分超過2ms, 也比較正常.
存活對象較大的堆
前面分析的是存活對象不多的情況.
Mark Sweep是根據root來找到所有存活對象, 雖然堆很大, 但存活的對象不多, 所以mark時間也不會很大. 如果存活對象很多, 比如像緩存服務, golang的gc是怎樣的情況呢?
測試方式
這里我寫了一個代碼來模擬.
全局一個map, 每個key大致15-20個字節, 每個value大概200-300字節, 啟動時構建map(模擬緩存服務).
有8個協程模擬調用, 每個協程每次分配10個4K的數組+100個50字節左右的數據, 通過qps參數可控制分配速率.
在8核, 16G Docker中運行. 圖中大概每G存活有280萬存活對象.
結果如下:
分析
由上可以看出, Golang GC Mark消耗的CPU時間與存活的對象數基本成正比(更準確的說是和需掃描的字節數, 每個對象第一個字節到對象的最后一個指針字段). 對于G級別以上的存活對象, 掃描一次需要花秒以上的CPU時間.
圖中當存活對象5G時(這時候存活1400萬個對象), Mark消耗的CPU時間為10.8s左右.Golang并發Mark的過程中, 會希望將25%的CPU用于并發Mark, 5%的CPU用于mutator輔助Mark, 總計大致30%左右的CPU用于Mark(如果沒有太多業務請求, 有空閑的P, 也會用于GC Mark).
如果此時業務請求很繁忙, CPU基本滿載, 那么對于8核的服務器, 只花25%-30%在GC Mark上, 即兩個核, 那么GC Mark時間會持續10/2=5s左右.如果基本沒有業務請求, 空閑的核也參與GC Mark, 那么GC Mark時間也需要持續10.8/8, 大致1.35s.
Golang GC在存活對象非常多的情況下, 對CPU吞吐量的降低還是比較多. 對于滿載的服務, GC Mark期間性能會降低30%-40%(還有寫屏障的性能損耗). 性能降低不可怕, 怕的是擴展性不好. 好消息是, Golang GC的可擴展性非常好. 不管100M, 1G, 10G, 20G, 50G, STW都基本不變, 且GC Mark的Clock Time隨著CPU增加會比例減少.不過對于減少GC Mark消耗的CPU, Golang團隊也在考慮實現Golang特色的分代GC.
Golang GC的一些演進規劃
標題有點標題黨, 我們就不談未來了, 我們談談Golang GC的一些規劃和嘗試. 這個部分主要參考Go的GC掌門人Richard L. Hudson在去年做的一個演講.
Getting to Go: The Journey of Go's Garbage Collector Getting to Go: The Journey of Go's Garbage Collector
Request Oriented Collector(面向請求的回收器)
在2016年有一個propose是, Request Oriented Collector(面向請求的GC), 簡單的說就是私有對象隨著請求的結束而消亡.
Go目前很大一個應用場景(Cloud Native?)是接受一個請求, 開一個協程, 進行處理, 處理完后協程回收, 在這個過程中大部分對象都是和本次請求相關的, 不會和其他協程共享, 也不會在處理過程結束后還存活. 可以將這個過程的對象分為兩種, 一種為private, 一種是shared.通過寫屏障來保證, 如果將private對象交給shared對象, 會遞歸的將該private對象引用的對象標記為shared對象, 請求結束private對象就被回收了.
在實現的過程中Go團隊發現, 開啟ROC大大減少了對象的publish, 在大型應用中具有很好的擴展性. 但是因為ROC需要寫屏障一直開啟(而Go原來的實現只需要在并發Mark階段開啟寫屏障), 大大降低了性能, 開啟ROC的Go編譯器使得編譯速度降低了30%-50%, 一些性能測試中, 也降低了30%多, 我們大部分應用都是在4-16核的小機器上, 這些性能損失是不可接受的. 也許在128核以上, ROC的擴展性帶來的好處大于寫屏障帶來的性能損失.
當前ROC這個嘗試被標記為failure.
https://blog.golang.org/ismmkeynote
Generational GC(分代GC)
對ROC嘗試上的失敗, Go團隊轉向分代GC.(從描述來看, 個人感覺ROC像是對分代GC的一種激進的做法. 主要思路也是為了減少對象的publish, 能回收掉的對象盡快回收掉.)
分代GC理論是80年代提出來的, 基于大部分對象都會在短時間內成為垃圾這個軟件工程上的事實, 將內存分為多個區, 不同的區采取不同的GC算法. 分代GC并不是一種GC算法, 而是一種策略.
Java的分代GC
Java GC是分代GC的忠實擁簇者, 從98年發布的Java 1.2開始, 就已經是分代GC了. 圖中是Java多種分代算法的默認分配情況.
圖中Eden區, from, to區統稱為新生代. 一般空間比較小, JVM中默認為堆的1/3.
對象優先分配在新生代(容量小, 放新對象)的Eden區, Minor GC時把Eden區的存活對象和From區對象一起復制到to區, GC完之后, 交換from和to區. 新對象到達一定年齡(經歷GC不被回收的次數)后才提升到老年代(容量大, 放長期存活的對象)中. 新生代和老生代采用不同的算法. 新生代采用復制(Copy)算法, 老生代則一般采用并發的Mark Sweep.
大部分時候只需要對新生代進行Minor GC,因為新生代空間小,Minor GC的暫停很短(生產服務中, 4核機器中500M-1G的新生代, GC大概為3ms-10ms的級別). 且絕大部分對象分配后就很快被Minor GC回收, 不會提升到老年代中, 對老年代Major GC的次數就會很少, 大大減少了頻繁進行Major GC而Scan和Mark消耗的CPU時間, 減少回收大堆而導致的大的STW時間頻次. 像API/RPC后臺服務, 比較穩定的話, 可能幾小時或一天才進行一次Major GC.
Golang的分代GC
為何Golang對分代GC需求相對不大?
Golang因為是面向值對象的, 而非Java那樣面向引用, 且逃逸分析能夠減少在堆中分配對象, 所以Golang天生產生的垃圾相對于Java比較少. 同時Golang的內存結構是類似TCMalloc多階緩存機制, 大大減少多線程競爭, 所以Golang的對象分配速度比較快(Java的新生代使用指針移動的方式進行分配, 吐吞量更高), 同時經過多個版本的演進, Golang GC的STW已經降到了sub ms, 所以一直以來Golang對于分代GC的需求并不是那么大.
Golang目前在嘗試引入分代GC的原因是目前Golang進行GC的頻次還是有點快, 而每次GC時Golang都需要Mark Scan所有存活的對象, 導致GC的CPU消耗比較高.
在實際生產中, 千QPS的服務, Java CMS可能幾個小時或一天才需要進行一次Major GC, Scan和Mark一遍堆中存活的對象, 而Go則為十多秒或分鐘就需要進行一次GC, 把所有存活的對象掃描一遍(雖然Golang的STW非常短)
分代GC策略中, 新生代一般是Copy算法, 這樣會移動對象, 而Golang之前GC是非移動的(Go中的對象可以直接傳入到C中, 所以不能GC前后不能移動對象位置), Go實現分代GC也需要是非移動的. 如何實現呢?
Golang分代GC實現
簡單的說, 就是GC后的gcMarkBits不清空, 對象存活為1. 那下一次GC時, 在還沒有進行標記時, 發現gcMarkBits為1, 那就是老對象, 為0, 就是新分配的對象.
在Minor GC中只需要對新生代對象進行清掃,老年代對象無論是否標記可達,都認為是存活的,不會進行清掃。同時還需要找出老年代對象對新生代對象的指向,對新生代對象進行提升的操作。
Java對新生代進行進行GC時, 需要把老年代作為新生代的Root Set(通過寫屏障和Card Table記錄). Golang也需要記錄有哪些老對象引用了新對象, 目前的方案是使用Card Hash without write barrier, 對于每一小塊內存, 使用硬件算法計算Hash, 如果兩次GC中, Hash改變了, 表明引用了新對象, 只對改變的塊進行掃描引用了哪些新對象.
至于何時開始GC, 何時做Minor GC, 何時為Full GC, 我也了解不是很清楚. 后面了解清楚再來補.
目前還只是有幾個CI, 并沒有真正release.
https://go-review.googlesource.com/c/go/+/137476/12
這里有一篇文章19年4月份對19年4月Golang分代GC狀況的描述. 上面的一張圖片來自此文.
http://www.lxweimin.com/p/2383743edb7b
未來的一些方向原則
增強runtime的魯棒性, 更加關注用戶提出的一些極端問題. 增強調度器的公平性.
Go會繼續保持暴露給用戶的GC參數的簡單. 除了GOGC(SetGCPercent)外, 后續會發布一個SetMaxHeap(已在內部使用).
Go將會繼續提升目前已經還不錯的逃逸分析和面向值對象的編程.
相對于增加CPU消耗(比如寫屏障)的方案, Go團隊會更傾向于占用內存多一些方案.因為Go團隊認為, CPU的摩爾定律發展已經減緩, 18個月翻倍減緩為2年,4年...而內存容量和價格的摩爾定律仍在繼續. 一個稍微更占用內存的解決方案比更占用CPU的解決方案擁有更好的擴展性.
Golang GC方面一些重要的proposal和設計文檔
Go 1.4+ Garbage Collection (GC) Plan and Roadmap
介紹Golang 1.4版本以后對并發GC的規劃
https://docs.google.com/document/d/16Y4IsnNRCN43Mx0NZc5YXZLovrHvvLhK_h0KN8woTO4/edit
Go 1.5 concurrent garbage collector pacing
介紹Golang 1.5并發GC設計原理
https://golang.org/s/go15gcpacing
Go 1.6 GC roadmap
在1.6版本解決1.5并發GC發布后一些設計不協調以及考慮不充分的地方
https://docs.google.com/document/d/1kBx98ulj5V5M9Zdeamy7v6ofZXX3yPziAf0V27A64Mo
Golang 1.7中實現, Proposal: Dense mark bits and sweep-free allocation
span中組織對象的方式由freelist的方式改成bitmap, GC時寫好了bitmap, sweep階段, 就不需要做多余的轉換.
https://github.com/golang/proposal/blob/master/design/12800-sweep-free-alloc.md
Request Oriented Collector (ROC) Algorithm: 一個面向請求的GC, 對象隨著請求誕生, 隨著請求消亡和回收
https://docs.google.com/document/d/1gCsFxXamW8RRvOe5hECz98Ftk-tcRRJcDFANj2VwCB0/edit
Golang 1.8中實現, Proposal: Eliminate STW stack re-scanning
Go 1.8消除stack re-scanning, 進入sub ms階段
https://github.com/golang/proposal/blob/master/design/17503-eliminate-rescan.md
Proposal: Simplify mark termination and eliminate mark 2: 遺留代碼清理, 簡化流程
https://github.com/golang/proposal/blob/master/design/26903-simplify-mark-termination.md
總結
本文先介紹GC的一些基本內容, 然后介紹Golang GC的發展歷史, 以及當前Golang GC的STW和Mark情況, 目前在生產上, Golang GC的sub ms的STW都不會成為問題. 需要注意的是如果頻繁GC導致Mark對象帶來的性能消耗.
Golang團隊仍在不斷的改進Golang的Runtime. 未來會繼續注重runtime的穩定性, 關注用戶提出的極端情況. 隨著CPU摩爾定律的減緩, Golang團隊會選擇減少CPU消耗的方案.