當前位置: 華文問答 > 數位

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

2016-04-09數位

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的更高,這算是個優勢。

    (待續)