現(xiàn)代的垃圾回收機(jī)制(Go 垃圾回收機(jī)制概述)
關(guān)于 Go GC策略的見解
細(xì)節(jié)你可以到 Hacker News 和 Reddit 查看相關(guān)內(nèi)容
最近我看到了很多關(guān)于Go 最近的垃圾回收機(jī)制的推廣文章韵丑,甚至有一些來自于Go項(xiàng)目組狡恬,從他們的文字中感受到,Go的垃圾回收機(jī)制似乎發(fā)生了根本性的突破当悔。
這里有一篇2015年8月份的畏妖,關(guān)于最新的收集器的介紹
Go 語言構(gòu)建了一個(gè)新的GC機(jī)制脉执,不只為了應(yīng)對2015,甚至2025年以及更遠(yuǎn)的時(shí)間。Go 1.5版本的GC 迎來了一個(gè)全新的改變戒劫,讓STW(Stop-the-world)的暫停不再是語言轉(zhuǎn)向安全可靠的語言的障礙半夷。在未來,應(yīng)用程序隨著硬件一起擴(kuò)展變得更容易迅细,GC不再是阻礙硬件變得更有強(qiáng)大的障礙巫橄,這是未來十年甚至更長時(shí)間上的一個(gè)突破。
同時(shí)Go的團(tuán)隊(duì)說茵典,新的GC不僅解決了暫停的問題湘换,而且讓這個(gè)事情變得更簡單。
此外,不受外界的影響彩倚,我們的進(jìn)行時(shí)團(tuán)隊(duì)可以專注于提高用戶程序反饋的真實(shí)問題筹我。
毫無疑問,Go的很多用戶對于這個(gè)新的運(yùn)行時(shí)機(jī)制非常的滿意署恍,但是我對于這種說法還是有一些疑慮 —— 對我來說崎溃,這種說法就像是一種虛幻(misleading)的推銷方式。由于這種說法在博客圈內(nèi)反復(fù)出現(xiàn)盯质,我們應(yīng)該對于這件事進(jìn)行深入的關(guān)注了袁串。
事實(shí)上,Go GC并沒有實(shí)現(xiàn)更多的新創(chuàng)意和新的研究成果呼巷。正如他們所公示的內(nèi)容囱修,他是一個(gè)很直接的并發(fā)標(biāo)記/掃描收集的機(jī)制,這種方式還是基于1970年的研究想法王悍。GC唯一不同的是他通過犧牲一些其他合理的性能來優(yōu)化暫停的時(shí)間破镰,但是GC的技術(shù)發(fā)言和推銷材料上,卻沒有提及他們犧牲了什么內(nèi)容压储,讓那些不熟悉或者不關(guān)心垃圾回收機(jī)制的人忘記了這段犧牲的內(nèi)容鲜漩,甚至暗示出,Go語言的其他競爭對手(Java等)的垃圾回收很差集惋。Go鼓勵以下這種看法:
為了創(chuàng)造一個(gè)應(yīng)用于未來10年的垃圾回收器孕似,我們研究了過去幾十年的算法內(nèi)容。Go的新垃圾回收機(jī)制刮刑,是采用并發(fā)的三色標(biāo)記法收集器喉祭,這個(gè)想法在1987年被Dijkstra提出。這個(gè)“企業(yè)級”垃圾回收期在今天依然是一個(gè)爭議很大的產(chǎn)品雷绢,但我們堅(jiān)持認(rèn)為泛烙,對于當(dāng)今時(shí)代的硬件設(shè)備,他是一個(gè)最適合的并且符合現(xiàn)代計(jì)算機(jī)對于延遲要求的產(chǎn)物翘紊。
讀完了上述的內(nèi)容蔽氨,你可能會覺得過去40年的企業(yè)級GC架構(gòu)根本沒有任何價(jià)值。
GC相關(guān)的理論讀物
關(guān)于設(shè)計(jì)一個(gè)GC的垃圾回收器帆疟,我們需要考慮一下方面:
- 程序的吞吐量:你的算法降低了多少程序性能孵滞,跟正常的工作相比,GC的時(shí)間占用了多少的百分比
- GC的吞吐量:單位時(shí)間內(nèi)鸯匹,GC可以清理多少垃圾坊饶。
- 堆的開銷:垃圾回收時(shí),需要多少額外的內(nèi)存需求。如果你的算法在收集垃圾的時(shí)候,需要額外的臨時(shí)結(jié)構(gòu),是否會讓你的程序內(nèi)存變得更緊張桑腮。
- 暫停時(shí)間:STW的時(shí)間
- 暫停的頻率:關(guān)于STW的頻率
- 暫停的分布情況:是否是有時(shí)候停頓很短痘绎,但有時(shí)候停頓很長津函,又或者停頓的時(shí)間稍微長一點(diǎn),但是清理的效果更好孤页。
- 分配的性能: 新內(nèi)存的分配是很快尔苦、很慢還是無法預(yù)測的。
- 壓縮情況: 當(dāng)有足夠的內(nèi)存的時(shí)候行施,但是每塊很小的時(shí)候允坚,是否也會報(bào)出OOM的錯誤(內(nèi)存過于碎片化),如果沒有的話,及時(shí)你的程序有足夠的內(nèi)存蛾号,也依然會變得更慢稠项,甚至死亡。
- 并發(fā)性:你的收集器是否利用了多線程的特性
- 伸縮性:當(dāng)你的堆內(nèi)存變得更大的時(shí)候鲜结,是否會改變工作策略展运。
- 調(diào)優(yōu): 對于開箱即用的情況,你的默認(rèn)配置能提供更好的性能精刷。
- 預(yù)熱(調(diào)整拗胜?)時(shí)間:你的算法是否會根據(jù)不同的情況自我調(diào)整,如果是的話怒允,有需要多久才能到達(dá)最佳狀態(tài)呢埂软。
- 迭代周期:你的算法是否會將釋放的內(nèi)存資源還給操作系統(tǒng),如果是的話误算,出發(fā)的時(shí)機(jī)是什么時(shí)候。
- 可移植性:你的GC工作機(jī)制是否是基于CPU的迷殿,是否可以保證在x86這樣的弱內(nèi)存情況下儿礼,保證正常的工作(一致性)
- 兼容性:你的收集工作是基于哪些編程語言的,是否能夠支持一些并未實(shí)現(xiàn)GC機(jī)制的編程語言使用庆寺,例如C++蚊夫,是否需要修改編輯器。如果是的話懦尝,那么更改GC算法是否需要重新編譯所有程序和依賴項(xiàng)
正如你所看到的知纷,設(shè)計(jì)垃圾收集器設(shè)計(jì)很多因素,其中一些因素甚至?xí)绊懩闫脚_下的一些生態(tài)的內(nèi)容陵霉。而且我也不確定我以上列舉的內(nèi)容是否有遺漏琅轧,可能還有更多未曾提及的內(nèi)容。
由于設(shè)計(jì)的復(fù)雜性踊挠,垃圾回收期一直是眾多計(jì)算機(jī)科學(xué)領(lǐng)域研究論文的一個(gè)子領(lǐng)域乍桂。學(xué)術(shù)界和工業(yè)界都在研究并實(shí)現(xiàn) 穩(wěn)定的速度 的新算法冲杀。但是不幸的是,沒有人研究出能夠適合所有場景的算法睹酌。
權(quán)衡無處不在
讓我們更具體的來聊聊
第一代垃圾回收算法被設(shè)計(jì)用于單核處理器权谁,很小堆的程序上面。那時(shí)候CPU和RAM非常貴憋沿,而且用戶要求的不高旺芽。所以肉眼可見的暫停也可以讓人接受。所以這個(gè)時(shí)代的算法辐啄,優(yōu)先考慮最小化的堆內(nèi)存和CPU開銷采章。這意味著,在你沒有分配新內(nèi)存的時(shí)候则披,GC什么也不做共缕,在需要的時(shí)候,他會啟動士复,然后程序暫停图谷,并且他需要完成所有的堆標(biāo)記,掃描阱洪,盡可能快的釋放一些不需要的局部區(qū)域便贵。
盡管這樣的垃圾收集器很舊,但是仍然有一定的學(xué)習(xí)價(jià)值冗荸。他非常簡單承璃,在不需要收集的時(shí)候,他不會拖慢你的程序蚌本,也不需要添加多余的堆內(nèi)存盔粹。對于像Boehm GC這樣的保守派的收集器,他們甚至不需要改變編輯器和編程語言程癌。這可以使它們適用于通常具有小堆的桌面應(yīng)用程序舷嗡,包括AAA視頻游戲,其中大部分RAM由不需要掃描的數(shù)據(jù)文件占用嵌莉。
Stop-the-world (STW) 標(biāo)記/掃描 是現(xiàn)在垃圾回收機(jī)制最常用的GC算法进萄,在進(jìn)行面試的時(shí)候,我經(jīng)常會讓候選人談?wù)凣C锐峭,但是不幸的是中鼠,候選人一般都把GC當(dāng)做一個(gè)黑盒,幾乎對他沒有任何了解沿癞,有的甚至認(rèn)為他是一個(gè)非常老的技術(shù)援雇。
問題是簡單的標(biāo)記掃描算法,性能很差椎扬。隨著你的核數(shù)和內(nèi)存變大熊杨,這個(gè)算法甚至?xí)V构ぷ魇镄瘛5牵ǔG闆r下晶府,通過更小的堆劃分桂躏,來控制暫停的時(shí)間也是足夠了。這種情況下川陆,你可能更希望使用這種方式來保證低消耗剂习。
另一種情況是,你可能用的是數(shù)十核 加上數(shù)百GB的機(jī)器较沪,你的服務(wù)可能在進(jìn)行復(fù)雜的金融交易或者執(zhí)行一個(gè)搜索引擎鳞绕,這種服務(wù)上面,低停頓對你來說可能很重要尸曼,在這些情況下们何,你可能更希望使用一種算法,在運(yùn)行時(shí)降低程序的速度控轿,以便于回收器在后臺進(jìn)行短暫停頓的收集冤竹。
這不是一個(gè)簡單的系列,在更大的后端里面茬射,可能有大批量的工作鹦蠕,非交互式的暫停無關(guān)緊要,只在意總的運(yùn)行時(shí)間在抛。這種情況下钟病,最好使用一個(gè)比較大吞吐量的算法來覆蓋他們呢。即完成有用的工作與收集時(shí)間的比例問題刚梭。
以上的三種情況肠阱,告訴了我們,沒有一個(gè)單一的垃圾回收算法能同時(shí)適用于所有的項(xiàng)目朴读,沒有一個(gè)編程語言能知道屹徘,你的程序是一個(gè)有很多計(jì)算任務(wù)的服務(wù),或者是一個(gè)對延遲很敏感的程序磨德。這就是GC 調(diào)優(yōu)存在的原因缘回,這并不是因?yàn)楣こ處熡薮肋菏樱瑐?cè)面反映了計(jì)算機(jī)科學(xué)能力上的一些限制典挑。
分代設(shè)計(jì)
在1984年,這個(gè)想法就被大家所熟知啦吧,就是收集器分配的 "年輕代"您觉,即在分配內(nèi)存后很快被回收的內(nèi)容。這種想法只是一種假設(shè)上的劃分授滓,也算是整個(gè)PL工程領(lǐng)域中最強(qiáng)大的劃分之一琳水。他在不同的編程語言和軟件行業(yè)數(shù)十年的變革中肆糕,始終如一。在很多函數(shù)式語言在孝,命令式語言诚啃,弱類型語言中,也是這樣做的私沮。
理解這個(gè)東西對于程序很有用始赎,因?yàn)镚C算法的設(shè)計(jì)或多或少都會參照這個(gè)思路。一些新的垃圾回收機(jī)制仔燕,在舊的 停止-標(biāo)記-掃描 的風(fēng)格上造垛,都有很多的改進(jìn)。
- GC吞吐量:他可以更快的收集更多的垃圾晰搀。
- 分配性能:分配新的內(nèi)存五辽,不需要在堆中進(jìn)行大面積的查找,所以分配效率上很高外恕。
- 程序吞吐量:分配器會動態(tài)的處理內(nèi)存間隙的問題(原文大概是這個(gè)意思)杆逗,這樣顯著的提升了緩存的利用率。分代收集器在這個(gè)上面做了很多額外的工作吁讨,但是給予緩存效果帶來了更大的提升髓迎,足以抵消的這種消耗。
- 暫停時(shí)間:大部分的暫停時(shí)間變得更短了建丧。
同樣的也帶來了一些負(fù)面的效果
- 兼容性:實(shí)現(xiàn)一個(gè)垃圾回收期需要有足夠的能力去在內(nèi)存中來回移動一些內(nèi)容排龄,并在程序的某些情況下,能夠在寫入指針操作的時(shí)候做一些額外的工作翎朱,這意味著GC必須與編譯器緊密地結(jié)合橄维,所以C++沒有垃圾回收期。
- 堆內(nèi)存:這種收集器需要在各種"空間"中來回復(fù)制拴曲,所以必須保證有足夠的空間可以復(fù)制争舞,這意味著收集器會產(chǎn)生一些額外的開銷。同時(shí)還需要維護(hù)各種指針之間相互的引用關(guān)系(譯者加:類似Java 和 C# 的可達(dá)性分析),這種方式更加進(jìn)一步的增加了內(nèi)存的開銷澈灼。
- 暫停的情況:由于GC的暫停不是很頻繁竞川,所以有時(shí)候需要做一次整個(gè)堆上面的標(biāo)記掃描(full GC)
- 調(diào)優(yōu):垃圾回收期引入了 年輕帶GC和原始區(qū)的概念,所以程序的性能對于空間大小非常敏感
- 預(yù)熱:為了響應(yīng)調(diào)優(yōu)的問題叁熔,一些收集器通過觀察運(yùn)行的程序狀態(tài)委乌,來動態(tài)的分配年輕代的大小,但是暫停的時(shí)間取決于程序運(yùn)行的時(shí)間荣回,但這個(gè)內(nèi)容在實(shí)際生產(chǎn)中很少有關(guān)注的遭贸。
然而整體上來說,好處是巨大的心软,基本上所有的現(xiàn)代的GC算法都是基于這個(gè)基礎(chǔ)上做的壕吹,分代回收器可以通過各種方式增強(qiáng)功能著蛙,典型的現(xiàn)代的GC算法,基本上都同時(shí)具備平行耳贬,并行踏堡,生成,壓縮的能力咒劲。
Go并發(fā)收集器
Go語言屬于一種普通的擁有值類型的解釋性語言暂吉,因此內(nèi)存訪問模式與C#相比,Go的當(dāng)代假設(shè)成立缎患,而.NET使用的是分代收集方式慕的。
事實(shí)上,go程序的request和Response更像是http服務(wù)器挤渔,go程序展現(xiàn)出了極強(qiáng)的行為肮街,go開發(fā)團(tuán)隊(duì)探索面向請求的收集起的開發(fā)模式。這就意味著他對于GC問題的內(nèi)核已經(jīng)發(fā)生了本質(zhì)的改變判导。GC可以為request/response 處理器確保他足夠大的新生代空間嫉父,所有的垃圾·都可以通過處理并符合要求
盡管如此,Go目前的GC還不是一個(gè)多代的模型眼刃,只是一個(gè)后臺普通的舊的標(biāo)記掃描模型绕辖。
這樣做也有一個(gè)好處,你可以獲得非常低的暫停時(shí)間擂红,但是幾乎所有的其他事情都會變得很糟糕仪际。通過我們以上的結(jié)論,不難看出:
- GC吞吐量:時(shí)間用來清理堆昵骤。簡單來說树碱,你程序用的內(nèi)存更多,內(nèi)存釋放的越慢变秦,相對于正常工作成榜,回收消耗的時(shí)間比例越大。唯一的不受影響的方法就是蹦玫,你的程序完全沒有并行化赎婚,那么你可以不受限制的把更多的內(nèi)核讓給GC使用。* 壓縮:由于沒有壓縮的機(jī)制樱溉,你的程序可能最后可能被內(nèi)存碎片沾滿挣输,我們在后面的時(shí)候,會討論堆碎片的問題饺窿。由于沒有壓縮歧焦,所以也無法從連續(xù)的內(nèi)存緩存中收益移斩。
- 程序吞吐量:因?yàn)槊總€(gè)周期內(nèi)肚医,GC需要做很多的工作绢馍,所以這樣會盜取CPU的時(shí)間,從而使程序變得很慢肠套。
- 暫停分發(fā):與你程序同時(shí)運(yùn)行的垃圾回收機(jī)制舰涌,都會遇到Java世界中著名的"并發(fā)模式故障", 你程序執(zhí)行過程產(chǎn)生的垃圾,比回收的速度快很多你稚。這種情況下瓷耙,運(yùn)行時(shí)機(jī)制別無選擇,只能停止整個(gè)程序的執(zhí)行刁赖,等待GC的一個(gè)大循環(huán)的完成搁痛,盡管Go聲稱他們的GC暫停時(shí)間很短,但這種說法只能適用于GC有足夠的CPU運(yùn)行時(shí)間和足夠的內(nèi)存空間鸡典。所以這種情況,此外彻况,Go編譯器缺乏讓所有線程快速暫停的能力舅踪,所以暫停時(shí)間很短這件事纽甘,很大程度上取決于你所執(zhí)行的代碼(例如,base64解碼單個(gè)goroutine中的大blob會導(dǎo)致暫停時(shí)間的上升)抽碌。
- 堆開銷:因?yàn)槭占鳂?biāo)記和掃描的時(shí)間很慢,所以需要大量的內(nèi)存空間货徙,來保證不發(fā)生上述的"并發(fā)模式故障". Go默認(rèn)的堆開銷為100%的百分比,這意味著程序每次執(zhí)行的所需內(nèi)存會直接翻倍破婆。
關(guān)于上述內(nèi)容的權(quán)衡問題涮总,我們可以看這篇文字:
由于Server 1 分配的內(nèi)存高于 Server 2, 所以STW的暫停時(shí)間1比2 更高,但是 暫停的持續(xù)時(shí)間祷舀,相對來說還是下降了一個(gè)維度,在切換兩個(gè)服務(wù)之后裳扯,CPU的使用量,增加了20%
所以在這個(gè)特殊的情況下饰豺,Go盡管暫停時(shí)間下降了,但是同樣也拖慢了收集器的速度蒿柳。這種方式是一個(gè)足夠平衡的模式?還是說暫停時(shí)間無法繼續(xù)優(yōu)化了垒探?官方?jīng)]有給與一個(gè)解釋。
但是這就產(chǎn)生了一個(gè)問題蛤克,付出更多的硬件來降低暫停時(shí)間是否有意義夷蚊,如果你的暫停時(shí)間從10ms 降到1ms,用戶是否可以真實(shí)感受到惕鼓?是否值得你擴(kuò)大兩倍的硬件去得到它。
Go 優(yōu)化暫停時(shí)間的方式呜笑,就是減慢你程序的執(zhí)行效率,來獲取更快的暫停凰慈。
與Java對比
HotSpot JVM 有眾多的GC算法供你選擇驼鹅,沒有像Go這樣處理暫停時(shí)間,因?yàn)樗麄儠?quán)衡他們GC算法之間的區(qū)別豺型,可以通過對比來比較誰好誰壞。只需要重啟你的程序买乃,并在GC算法中做選擇,通過實(shí)踐決定你用哪一個(gè)算法來執(zhí)行你的程序剪验。
任何現(xiàn)代的計(jì)算機(jī),默認(rèn)的收集算法都是吞吐量優(yōu)先的收集器娶眷。這個(gè)是為了批量作業(yè)而設(shè)計(jì)的啸臀,默認(rèn)情況下沒有暫停時(shí)間的目標(biāo)。之所以選擇這個(gè)算法作為默認(rèn)算法的原因是,所有人都開箱即用伤塌。所以Java只能讓你的程序盡可能快的運(yùn)行幌羞,減少內(nèi)存開銷和暫停時(shí)間竟稳。
如果你對于暫停時(shí)間特別敏感,那推薦你選擇(CMS)的方式他爸,這個(gè)是跟Go語言使用的最接近的算法诊笤,但是CMS同樣也是基于分代策略的,這也是為什么他暫停時(shí)間比Go長的原因:
年輕代因壓縮而應(yīng)用暫停讨跟,因?yàn)樗枰苿觾?nèi)存。CMS有兩種暫停類型茶袒,一種是比較快的機(jī)制凉馆,大概2-5 ms,另一種大概要20ms澜共,CMS是一種自適應(yīng)的模型,因?yàn)樗遣l(fā)的母谎,他必須要像Go一樣猜測執(zhí)行的時(shí)機(jī)京革。Go建議你配置堆的開銷來進(jìn)行調(diào)整,而CMS是在運(yùn)行時(shí)進(jìn)行自適應(yīng)存崖,來防止并發(fā)模式失敗。由于是普通的標(biāo)記掃描方式冗栗,所以碎片化會導(dǎo)致減速的問題。
Java最新的垃圾回收期叫做"G1",寓意是"garbage first", 他并不是Java 8 默認(rèn)的機(jī)制钠至,在Java 9 之后會被設(shè)置為默認(rèn)的機(jī)制胎源。這種算法希望能夠?qū)崿F(xiàn)一刀切的方式。它主要是整個(gè)堆上面的并發(fā)涕蚤,壓縮和生成。他同樣的也是自我調(diào)整的佑钾。但是就像所有的GC算法無法理解你的真實(shí)需求一樣烦粒,他只能通過一些配置的方式,幫你完成一些平衡的操作扰她。你只需要告訴他最大的RAM量和允許的最大暫停時(shí)間,他會自我調(diào)整其他的參數(shù)禾进,來保證你的要求廉涕。默認(rèn)的最大暫停時(shí)間是100ms。如果你不調(diào)整這些內(nèi)容狐蜕,G1的默認(rèn)思路就是讓你的應(yīng)用運(yùn)行的更快,暫停的更短婆瓜。
同樣的暫停的時(shí)間也不都是一致的贡羔,大多數(shù)情況下,都非澈秕澹快(小于1ms)楣嘁,當(dāng)堆被壓縮的時(shí)候珍逸,會比較慢(50ms左右), G1的伸縮性很好聋溜,有報(bào)道稱在TB級別的堆數(shù)據(jù)中,依然表現(xiàn)良好撮躁,而且還有一些比較酷的能力,例如在堆中復(fù)制字符串杨帽。
最后祝迂,一種名為Shenandoah的新GC算法器净。 它是OpenJDK貢獻(xiàn)的,但除非您使用Red Hat(贊助項(xiàng)目)的特殊Java構(gòu)建纠俭,否則不會使用Java 9浪慌。 無論堆大小如何,它都可以提供非常低的暫停時(shí)間权纤,同時(shí)仍然可以進(jìn)行壓縮。 成本是額外的堆開銷和更多障礙:在應(yīng)用程序仍在運(yùn)行時(shí)移動對象需要指針讀取和寫入以與GC交互外邓。 在這個(gè)意義上古掏,它類似于Azul的“無動作”收藏家。
結(jié)論
本文的主要觀點(diǎn)并不是為了說服你使用不同的編程語言和工具丧枪,只為了讓你理解一件事庞萍,GC是一個(gè)難題,真的非常難钝计,幾十年來一直都由眾多的計(jì)算機(jī)科學(xué)家不斷研究服赎。所以
不要懷疑或者相信別人錯過了某些思想上的突破交播,他們可能只是通過一些偽裝或者不同尋常手段來平衡得失,避免一些可能帶來的原因而做出努力缺厉。
如果你希望犧牲一切來達(dá)到最小的暫停時(shí)間的話隧土,那強(qiáng)烈建議你,閱讀GoGC
原文鏈接:Modern garbage collection
作者:Mike Hearn
譯者:JYSDeveloper