Go1.6中的gc pause已經完全超越JVM了嗎?

來自:garbage collection

在200G的heap下,Go居然能控制 STW 在20ms以下,遠遠超過G1 200G heap的表現。 go 的gc超越JVM了嗎,go是否大量犧牲了吞吐量換取了pause?

----------------- UPDATE ---------------------

Golang的對象是可以分配在棧上的,而java必須分配在堆上,這是不是意味著go會產生比 java更少的內存垃圾


Go的GC完勝JVM GC?

作為在2TB的GC堆上能維持在&< 10ms GC暫停時間的Azul Systems的Zing JVM…

而且Zing JVM的C4 GC是一種完全並發的、會整理堆內存的GC(Fully Concurrent Mark-Compact GC),不但mark階段可以是並發的,在整理(compaction)階段也是並發的,所以在GC堆內不會有內存碎片化問題;而Go 1.5/1.6GC是一種部分並發的、不整理堆內存的GC(Mostly-Concurrent Mark-Sweep),雖然實現已經做了很多優化但終究還是能有導致堆內存碎片化的workload,當碎片化嚴重時Go GC的性能就會下降。

簡短回答是:不,Go 1.6的GC並沒有在GC pause方面「完勝」JVM的GC。

我們有實際客戶在單機十幾TB內存的伺服器上把Zing JVM這2TB GC堆的支持推到了極限,有很多Java對象,外帶自己寫的基於NIO的native memory內存管理器,讓一些Java對象後面掛著總共10TB左右的native memory,把這伺服器的能力都用上了。在這樣的條件下Zing JVM的GC還是可以輕鬆維持在&< 5ms的暫停時間,根本沒壓力;倒是Linux上自帶的glibc的ptmalloc2先「掛」了——它不總是及時歸還從OS申請來的內存,結果把這沒開swap的伺服器給跑掛了…

(注意上面的單位都是TB。)

Zing JVM的C4 GC跟其它JVM GC相比,最大的特徵其實還不是它「不暫停」(或者說只需要暫停非常非常短的時間),而是它對運行的Java程序的特徵不敏感,可以對各種不同的workload都保持相同的暫停時間表現。這樣要放在前面強調,因為下面的討論就要涉及workload了。

後面再補充點關於Zing JVM的GC的討論。先放幾個傳送門:

  • Azul Systems 是家什麼樣的公司? - RednaxelaFX 的回答

  • Java 大內存應用(10G 以上)會不會出現嚴重的停頓? - RednaxelaFX 的回答

  • C++ 短期內在華爾街的買方和賣方還是唯一選擇嗎? - RednaxelaFX 的回答

要跟JVM比GC性能的話不要光看HotSpot VM啊。

Go的低延遲GC的適用場景和實際性能如何?

其實很重要的注意點就是:每種GC都有自己最舒服的workload類型——Zing的C4 GC是少有的例外。

題主給的那張演示稿沒有指出這benchmark測的是啥類型的workload,也沒有說明這個workload運行了多長時間,這數據對各種不同情況到底有多少代表性還值得斟酌。最公平的做法是把benchmark用的Go程序移植到Java,然後用HotSpot VM的CMS GC也跑跑看,對比一下。

作為一種CMS(Mostly-Concurrent Mark-Sweep)GC實現,Go的GC最舒服的應用場景是當程序自身的分配行為不容易導致碎片堆積,並且程序分配新對象的速度不太高的情況。

而如果遇到一個程序會導致碎片逐漸堆積,並且/或者程序的分配速度非常高的時候,Go的CMS就會跟不上,從而掉進長暫停的深淵。這就涉及到低延遲模式能撐多久多問題。

具體怎樣的情況會導致碎片堆積大家有興趣的話我回頭可以來補充。主要是跟對象大小的分布、對象之間的引用關係的特徵、對象生命期的特徵相關的。

這裡讓我舉個跟Go沒關係的例子來說明討論這類問題時要小心的陷阱。

要評測JVM/JDK性能,業界有幾個常用的標準benchmark,例如SPECjvm98 / SPECjvm2008,SPECjbb2005 / SPECjbb2013,DaCapo等。其中有不少benchmark都是,其聲稱要測試的東西,跟它實際運行中的瓶頸其實並不一致。

SPECjbb2005就是個很出名的例子。JVM實現者們很快就發現,這玩兒實際測的其實是GC暫停時間——如果能避免在測試過程中發生full GC,成績就會不錯。於是大家一股腦的都給自己的GC添加啟發條件,讓JVM實現們能剛剛好在SPECjbb2005的測試時間內不發生full GC——但其實很多此類「調優」的真相是只要在多運行那麼幾分鐘可能就要發生很長時間的full GC暫停了。

所以說要討論一個GC的性能水平如何,不能只靠看別人說在某個沒有註明的workload下的表現,而是得具體看這個workload的特徵、運行時間長度以及該GC的內部統計數據所表現出的「健康程度」再來綜合分析。

Go CMS GC與HotSpot CMS GC的實現的比較

Go GC目前的掌舵人是Richard L. Hudson大大,是個靠譜的人。

他之前就有過設計並發GC的經驗,設計了Sapphire GC演算法。

Sapphire: Copying GC Without Stopping the World

ftp://ftp.cs.umass.edu/pub/osl/papers/sapphire-2003.pdf

設計了並發Copying GC的他在Go里退回到用CMS感覺實屬無奈。雖然未來Go可能會嘗試用能移動對象的GC,在Go 1.5的時候它的GC還是不移動對象的,而外部跟Go交互的C代碼也多少可能依賴了這個性質。要不移動對象做並發GC,最終就會得到某種形式的CMS。

Go的CMS實現得比較細緻的地方是它的pacing heuristics,或者說「並發GC的啟動時機」。這是屬於「策略」(policy)方面做得細緻。HotSpot VM的CMS GC則這麼多年來都沒得到足夠多的關愛,其實尚未發揮出其完全的能力,還有不少改進/細化的餘地,特別是在策略方面。

而在「機制」(mechanism)方面,Go的CMS GC其實與HotSpot VM的CMS GC相比是非常相似的。都是只基於incremental update系write-barrier的Mostly-Concurrent Mark-Sweep。兩者的工作流程中最核心的步驟都是:

  1. Initial marking:掃描根集合
  2. Concurrent marking:並發掃描整個堆
  3. Re-marking:重新掃描在(2)的過程中發生了變化/可能遺漏了的引用
  4. Concurrent sweeping

具體到實現,兩者在上述核心工作流程上有各自不同的擴展/優化。

兩者的(1)都是stop-the-world的,這是兩者的GC暫停的主要來源之一。

HotSpot VM的CMS GC的(3)也是stop-the-world的,而且這個暫停還經常比(1)的暫停時間要更長;Go 1.6 CMS GC則在此處做了比較細緻的實現,儘可能只一個個goroutine暫停而不全局暫停——只要不是全局暫停都不算在用戶關心的「暫停時間」里,這樣Go版就比HotSpot版要做得好了。

(無獨有偶,Android Runtime(ART)也有一個CMS GC實現,而它也選擇了把上述兩種暫停中的一個變為了每個線程輪流暫停而不是全局暫停,不過它是在(1)這樣做的,而不是在(3)——這是Android 5.0時的狀況。新版本我還沒看)

HotSpot版CMS對(3)的細化優化是,在真正進入stop-the-world的re-marking之前,先嘗試做一段時間的所謂並發的「abortable concurrent pre-cleaning」,嘗試並發的追趕應用程序對引用關係的改變,以便縮短re-marking的暫停時間。不過這裡實現得感覺還不夠好,還可以繼續改進的。

有個有趣的細節,Go版CMS在(3)中重新掃描goroutine的棧時,只需要掃描靠近棧頂的部分棧幀,而不需要掃描整個棧——因為遠離棧頂的棧幀可能在(2)的過程中根本沒改變過,所以可以做特殊處理;HotSpot版CMS在(3)中掃描棧時則需要重新掃描整個棧,沒抓住機會減少掃描開銷。Go版CMS就是在眾多這樣的細節上比HotSpot版的更細緻。

再舉個反過來的細節。目前HotSpot VM里所有GC都是分代式的,CMS GC在這之中屬於一個old gen GC,只收集old gen;與其配套使用的還有專門負責收集young gen的Parallel New GC(ParNew),以及當CMS跟不上節奏時備份用的full GC。分代式GC很自然的需要使用write barrier,而CMS GC的concurrent marking也需要write barrier。HotSpot VM就很巧妙的把這兩種需求的write barrier做在了一起,共享一個非常簡單而高效的write barrier實現。

Go版CMS則需要在不同階段開啟或關閉write barrier,實現機制就會稍微複雜一點點,write barrier的形式也稍微慢一點點。

從效果看Go 1.6的CMS GC做得更好,但HotSpot VM的CMS GC如果有更多投入的話也完全可以達到一樣的效果;並且,得益於分代式GC,HotSpot VM的CMS GC目前能承受的對象分配速度比Go的更高,這算是個優勢。

(待續)


只跟Hotspot比,不和azul這種變態比,

從效果看Go 1.6的CMS GC做得更好,但HotSpot VM的CMS GC如果有更多投入的話也完全可以達到一樣的效果;並且,得益於分代式GC,HotSpot VM的CMS GC目前能承受的對象分配速度比Go的更高,這算是個優勢。

出處見最高票回答。


期待R大的續集,請再比較下G1,或者Redhat 給OpenJDK捐獻的 Shenandoah 下一代GC,會否更逆天。


不太懂,但是如果要對比的話,至少把測試的場景和jvm對應的曲線畫在一起才可以吧。最後從碼農的角度看,這種比較必須分場景,細分。就像拳擊比賽比的不是誰出拳重,你光列個出拳力量大小比較 沒意義。


推薦閱讀:

問下, C++ 的垃圾回收機制 如何實現的話 要怎麼掃描 bss data 之類的欄位?
XMLHttpRequest對象的生命周期是如何管理的?
為什麼 C++ 11 標準不加入 GC 功能呢?
c++11標準 GC(垃圾回收)是否會使老代碼產生未定義行為?
一個人基於OpenJDK實現GC的concurrent compact部分,以減少GC停頓,困難嗎?

TAG:Java虛擬機JVM | GC垃圾回收計算機科學 | Go語言 |