JVM 性能優化, Part 4: C4 垃圾回收

ImportNew注:本文是JVM性能優化 系列-第4篇。前3篇文章請參考文章結尾處的JVM優化系列文章。作爲Eva Andreasson的JVM性能優化系列的第4篇,本文將對C4垃圾回收器進行介紹。使用C4垃圾回收器可以有效提升對低延遲有要求的企業級Java應用程序的伸縮性。

到目前爲止,本系列的文章將stop-the-world式的垃圾回收視爲影響Java應用程序伸縮性的一大障礙,而伸縮性又是現代企業級Java應用程序開發的基礎要求,因此這一問題亟待改善。幸運的是,針對此問題,JVM中已經出現了一些新特性,所使用的方式或是對stop-the-world式的垃圾回收做微調,或是消除冗長的暫停(這樣更好些)。在一些多核系統中,內存不再是稀缺資源,因此,JVM的一些新特性就充分利用多核系統的潛在優勢來增強Java應用程序的伸縮性。

在本文中,我將着重介紹C4算法,該算法是Azul System公司中無暫停垃圾回收算法的新成果,目前只在Zing JVM上得到實現。此外,本文還將對Oracle公司的G1垃圾回收算法和IBM公司的Balanced Garbage Collection Policy算法做簡單介紹。希望通過對這些垃圾回收算法的學習可以擴展你對Java內存管理模型和Java應用程序伸縮性的理解,並激發你對這方面內容的興趣以便更深入的學習相關知識。至少,你可以學習到在選擇JVM時有哪些需要關注的方面,以及在不同應用程序場景下要注意的事項。

C4算法中的併發性

Azul System公司的C4(Concurrent Continuously Compacting Collector,譯者注,Azul官網給出的名字是Continuously Concurrent Compacting Collector)算法使用獨一無二而又非常有趣的方法來實現低延遲的分代式垃圾回收。相比於大多數分代式垃圾回收器,C4的不同之處在於它認爲垃圾回收並不是什麼壞事(即應用程序產生垃圾很正常),而壓縮是不可避免的。在設計之初,C4就是要犧牲各種動態內存管理的需求,以滿足需要長時間運行的服務器端應用程序的需求。

C4算法將釋放內存的過程從應用程序行爲和內存分配速率中分離出來,並加以區分。這樣就實現了併發運行,即應用程序可以持續運行,而不必等待垃圾回收的完成。其中的併發性是關鍵所在,正是由於併發性的存在纔可以使暫停時間不受垃圾回收週期內堆上活動數據數量和需要跟蹤與更新的引用數量的影響,將暫停時間保持在較低的水平。正如我在本系列第3篇中介紹的一樣,大多數垃圾回收器在工作週期內都包含了stop-the-world式的壓縮過程,這就是說應用程序的暫停時間會隨活動數據總量和堆中對象間引用的複雜度的上升而增加。使用C4算法的垃圾回收器可以併發的執行壓縮操作,即壓縮與應用程序線程同時工作,從而解決了影響JVM伸縮性的最大難題。

實際上,爲了實現併發性,C4算法改變了現代Java企業級架構和部署模型的基本假設。想象一下擁有數百GB內存的JVM會是什麼樣的:

  • 部署Java應用程序時,對伸縮性的要求無需要多個JVM配合,在單一JVM實例中即可完成。這時的部署是什麼樣呢?
  • 有哪些以往因GC限制而無法在內存存儲的對象?
  • 那些分佈式集羣(如緩存服務器、區域服務器,或其他類型的服務器節點)會有什麼變化?當可以增加JVM內存而不會對應用程序響應時間造成負面影響時,傳統的節點數量、節點死亡和緩存丟失的計算會有什麼變化呢?

C4算法的3的階段

C4算法的一個基本假設是“垃圾回收不是壞事”和“壓縮不可避免”。C4算法的設計目標是實現垃圾回收的併發與協作,剔除stop-the-world式的垃圾回收。C4垃圾回收算法包含一下3個階段:

  1. 標記(Marking) — 找到活動對象
  2. 重定位(Relocation) — 將存活對象移動到一起,以便可以釋放較大的連續空間,這個階段也可稱爲“壓縮(compaction)”
  3. 重映射(Remapping) — 更新被移動的對象的引用。

下面的內容將對每個階段做詳細介紹。

C4算法中的標記階段

在C4算法中,標記階段(marking phase)使用了併發標記(concurrent marking)和引用跟蹤(reference-tracing)的方法來標記活動對象,這方面內容已經在本系列的第3篇中介紹過。

在標記階段中,GC線程會從線程棧和寄存器中的活動對象開始,遍歷所有的引用,標記找到的對象,這些GC線程會遍歷堆上所有的可達(reachable)對象。在這個階段,C4算法與其他併發標記器的工作方式非常相似。

C4算法的標記器與其他併發標記器的區別也是始於併發標記階段的。在併發標記階段中,如果應用程序線程修改未標記的對象,那麼該對象會被放到一個隊列中,以備遍歷。這就保證了該對象最終會被標記,也因爲如此,C4垃圾回收器或另一個應用程序線程不會重複遍歷該對象。這樣就節省了標記時間,消除了遞歸重標記(recursive remark)的風險。(注意,長時間的遞歸重標記有可能會使應用程序因無法獲得足夠的內存而拋出OOM錯誤,這也是大部分垃圾回收場景中的普遍問題。)

Figure 1. Application threads traverse the heap just once during marking

如果C4算法的實現是基於髒卡表(dirty-card tables)或其他對已經遍歷過的堆區域的讀寫操作進行記錄的方法,那垃圾回收線程就需要重新訪問這些區域做重標記。在極端條件下,垃圾回收線程會陷入到永無止境的重標記中 —— 至少這個過程可能會長到使應用程序因無法分配到新的內存而拋出OOM錯誤。但C4算法是基於LVB(load value barrier)實現的,LVB具有自愈能力,可以使應用程序線程迅速查明某個引用是否已經被標記過了。如果這個引用沒有被標記過,那麼應用程序會將其添加到GC隊列中。一旦該引用被放入到隊列中,它就不會再被重標記了。應用程序線程可以繼續做它自己的事。

髒對象(dirty object)和卡表(card table)
由於某些原因(例如在一個併發垃圾回收週期中,對象被修改了),垃圾回收器需要重新訪問某些對象,那麼這些對象髒對象(dirty object)。這這些髒對象,或堆中髒區域的引用,通過會記錄在一個專門的數據結構中,這就是卡表。

在C4算法中,並沒有重標記(re-marking)這個階段,在第一次便利整個堆時就會將所有可達對象做標記。因爲運行時不需要做重標記,也就不會陷入無限循環的重標記陷阱中,由此而降低了應用程序因無法分配到內存而拋出OOM錯誤的風險。

C4算法中的重定位 —— 應用程序線程與GC的協作

C4算法中,*重定位階段(reloacation phase)*是由GC線程和應用程序線程以協作的方式,併發完成的。這是因爲GC線程和應用程序線程會同時工作,而且無論哪個線程先訪問將被移動的對象,都會以協作的方式幫助完成該對象的移動任務。因此,應用程序線程可以繼續執行自己的任務,而不必等待整個垃圾回收週期的完成。

正如Figure 2所示,碎片內存頁中的活動對象會被重定位。在這個例子中,應用程序線程先訪問了要被移動的對象,那麼應用程序線程也會幫助完成移動該對象的工作的初始部分,這樣,它就可以很快的繼續做自己的任務。虛擬地址(指相關引用)可以指向新的正確位置,內存也可以快速回收。

Figure 2. A page selected for relocation and the empty new page that it will be moved to

如果是GC線程先訪問到了將被移動的對象,那事情就簡單多了,GC線程會執行移動操作的。如果在重映射階段(re-mapping phase,後續會提到)也訪問這個對象,那麼它必須檢查該對象是否是要被移動的。如果是,那麼應用程序線程會重新定位這個對象的位置,以便可以繼續完成自己任務。(對大對象的移動是通過將該對象打碎再移動完成的。如果你對這部分內容感興趣的話,推薦你閱讀一下相關資源中的這篇白皮書“C4: The Continuously Concurrent Compacting Collector”)

當所有的活動對象都從某個內存也中移出後,剩下的就都是垃圾數據了,這個內存頁也就可以被整體回收了。正如Figure 2中所示。

關於清理
在C4算法中並沒有清理階段(sweep phase),因此也就不需要這個在大多數垃圾回收算法中比較常用的操作。在指向被移動的對象的引用都更新爲指向新的位置之前,from頁中的虛擬地址空間必須被完整保留。所以C4算法的實現保證了,在所有指向這個頁的引用處於穩定狀態前,所有的虛擬地址空間都會被鎖定。然後,算法會立即回收物理內存頁。

很明顯,無需執行stop-the-world式的移動對象是有很大好處的。由於在重定位階段,所有活動對象都是併發移動的,因此它們可以被更有效率的放入到相鄰的地址中,並且可以充分的壓縮。通過併發執行重定位操作,堆被壓縮爲連續空間,也無需掛起所有的應用程序線程。這種方式消除了Java應用程序訪問內存的傳統限制(更多關於Java應用程序內存模型的內容參見ImportNew編譯整理的第一篇《JVM性能優化, Part 1 ―― JVM簡介》)。

經過上述的過程後,如何更新引用呢?如何實現一個非stop-the-world式的操作呢?

C4算法中的重映射

在重定位階段,某些指向被移動的對象的引用會自動更新。但是,在重定位階段,那些指向了被移動的對象的引用並沒有更新,仍然指向原處,所以它們需要在後續完成更新操作。C4算法中的重映射階段(re-mapping phase)負責完成對那些活動對象已經移出,但仍指向那些的引用進行更新。當然,重映射也是一個協作式的併發操作。

Figure 3中,在重定位階段,活動對象已經被移動到了一個新的內存頁中。在重定位之後,GC線程立即開始更新那些仍然指向之前的虛擬地址空間的引用,將它們指向那些被移動的對象的新地址。垃圾回收器會一直執行此項任務,直到所有的引用都被更新,這樣原先虛擬內存空間就可以被整體回收了。

Figure 3. Whatever thread finds an invalid address enables an update to the correct new address

但如果在GC完成對所有引用的更新之前,應用程序線程想要訪問這些引用的話,會出現什麼情況呢?在C4算法中,應用程序線程可以很方便的幫助完成對引用進行更新的工作。如果在重映射階段,應用程序線程訪問了處於非穩定狀態的引用,它會找到該引用的正確指向。如果應用程序線程找到了正確的引用,它會更新該引用的指向。當完成更新後,應用程序線程會繼續自己的工作。

協作式的重映射保證了引用只會被更新一次,該引用下的子引用也都可以指向正確的新地址。此外,在大多數其他GC實現中,引用指向的地址不會被存儲在該對象被移動之前的位置;相反,這些地址被存儲在一個堆外結構(off-heap structure)中。這樣,無需在對所有引用的更新完成之前,再花費精力保持整個內存頁完好無損,這個內存頁可以被整體回收。

C4算法真的是無暫停的麼?

在C4算法的重映射階段,正在跟蹤引用的線程僅會被中斷一次,而這次中斷僅僅會持續到對該引用的檢索和更新完成,在這次中斷後,線程會繼續運行。相比於其他併發算法來說,這種實現會帶來巨大的性能提升,因爲其他的併發立即回收算法需要等到每個線程都運行到一個安全點(safe point),然後同時掛起所有線程,再開始對所有的引用進行更新,完成後再恢復所有線程的運行。

對於併發壓縮垃圾回收器來說,由於垃圾回收所引起的暫停從來都不是問題。在C4算法的重定位階段中,也不會有再出現更糟的碎片化場景了。實現了C4算法的垃圾回收器也不會出現背靠背(back-to-back)式的垃圾回收週期,或者是因垃圾回收而使應用程序暫停數秒甚至數分鐘。如果你曾經體驗過這種stop-the-world式的垃圾回收,那麼很有可能是你給應用程序設置的內存太小了。你可以試用一下實現了C4算法的垃圾回收器,併爲其分配足夠多的內存,而完全不必擔心暫停時間過長的問題。

評估C4算法和其他可選方案

像往常一樣,你需要針對應用程序的需求選擇一款JVM和垃圾回收器。C4算法在設計之初就是無論堆中活動數據有多少,只要應用程序還有足夠的內存可用,暫停時間都始終保持在較低的水平。正因如此,對於那些有大量內存可用,而對響應時間比較敏感的應用程來說,選擇實現了C4算法的垃圾回收器正是不二之選。

而對於那些要求快速啓動,內存有限的客戶端應用程序來說,C4就不是那麼適用。而對於那些對吞吐量有較高要求的應用程序來說,C4也並不適用。真正能夠發揮C4威力的是那些爲了提升應用程序工作負載而在每臺服務器上部署了4到16個JVM實例的場景。此外,如果你經常要對垃圾回收器做調優的話,那麼不妨考慮一下使用C4算法。綜上所述,當響應時間比吞吐量佔有更高的優先級時,C4是個不錯的選擇。而對那些不能接受長時間暫停的應用程序來說,C4是個理想的選擇。

如果你正考慮在生產環境中使用C4,那麼你可能還需要重新考慮一下如何部署應用程序。例如,不必爲每個服務器配置16個具有2GB堆的JVM實例,而是使用一個64GB的JVM實例(或者增加一個作爲熱備份)。C4需要儘可能大的內存來保證始終有一個空閒內存頁來爲新創建的對象分配內存。(記住,內存不再是昂貴的資源了!)

如果你沒有64GB,128GB,或1TB(或更多)內存可用,那麼分佈式的多JVM部署可能是一個更好的選擇。在這種場景中,你可以考慮使用Oracle HotSpot JVM的G1垃圾回收器,或者IBM JVM的平衡垃圾回收策略(Balanced Garbage Collection Policy)。下面將對這兩種垃圾回收器做簡單介紹。

Gargabe-First (G1) 垃圾回收器

G1垃圾回收器是新近纔出現的垃圾回收器,是Oracle HotSpot JVM的一部分,在最近的JDK1.6版本中首次出現(譯者注,該文章寫於2012-07-11)。在啓動Oracle JDK時附加命令行選項-XX:+UseG1GC,可以啓動G1垃圾回收器。

與C4類似,這款標記-清理(mark-and-sweep)垃圾回收器也可作爲對低延遲有要求的應用程序的備選方案。G1算法將堆分爲固定大小區域,垃圾回收會作用於其中的某些區域。在應用程序線程運行的同時,啓用後臺線程,併發的完成標記工作。這點與其他併發標記算法相似。

G1增量方法可以使暫停時間更短,但更頻繁,而這對一些力求避免長時間暫停的應用程序來說已經足夠了。另一方面,正如在本系列的[Part 3][4]中介紹的,使用G1垃圾回收器需要針對應用程序的實際需求做長時間的調優,而其GC中斷又是stop-the-world式的。所以對那些對低延遲有很高要求的應用程序來說,G1並不是一個好的選擇。進一步說,從暫停時間總長來看,G1長於CMS(Oracle JVM中廣爲人知的併發垃圾回收器)。

G1使用拷貝算法(在Part 3中介紹過)完成部分垃圾回收任務。這樣,每次垃圾回收器後,都會產生完全可用的空閒空間。G1垃圾回收器定義了一些區域的集合作爲年輕代,剩下的作爲老年代。

G1已經吸引了足夠多的注意,引起了不小的轟動,但是它真正的挑戰在於如何應對現實世界的需求。正確的調優就是其中一個挑戰 —— 回憶一下,對於動態應用程序負載來說,沒有永遠“正確的調優”。一個問題是如何處理與分區大小相近的大對象,因爲剩餘的空間會成爲碎片而無法使用。還有一個性能問題始終困擾着低延遲垃圾回收器,那就是垃圾回收器必須管理額外的數據結構。就我來說,使用G1的關鍵問題在於如何解決stop-the-world式垃圾回收器引起的暫停。Stop-the-world式的垃圾回收引起的暫停使任何垃圾回收器的能力都受制於堆大小和活動數據數量的增長,對企業級Java應用程序的伸縮性來說是一大困擾。

IBM JVM的平衡垃圾回收策略(Balanced Garbage Collection Policy)

IBM JVM的平衡垃圾回收(Balanced Garbage Collection BGC)策略通過在啓動IBM JDK時指定命令行選項-Xgcpolicy:balanced來啓用。乍一看,BGC很像G1,它也是將Java堆劃分成相同大小的空間,稱爲區間(region),執行垃圾回收時會對每個區間單獨回收。爲了達到最佳性能,在選擇要執行垃圾回收的區間時使用了一些啓發性算法。BGC中關於代的劃分也與G1相似。

IBM的平衡垃圾回收策略僅在64位平臺得到實現,是一種NUMA架構(Non-Uniform Memory Architecture),設計之初是爲了用於具有4GB以上堆的應用程序。由於拷貝算法或壓縮算法的需要,BGC的部分垃圾回收工作是stop-the-world式的,並非完全併發完成。所以,歸根結底,BGC也會遇到與G1和其他沒有實現併發壓縮選法的垃圾回收器相似的問題。

結論:回顧

C4是基於引用跟蹤的、分代式的、併發的、協作式垃圾回收算法,目前只在Azul System公司的Zing JVM得到實現。C4算法的真正價值在於:

  • 消除了重標記可能引起的重標記無限循環,也就消除了在標記階段出現OOM錯誤的風險。
  • 壓縮,以自動、且不斷重定位的方式消除了固有限制:堆中活動數據越多,壓縮所引起的暫停越長。
  • 垃圾回收不再是stop-the-world式的,大大降低垃圾回收對應用程序響應時間造成的影響。
  • 沒有了清理階段,降低了在完成GC之前就因爲空閒內存不足而出現OOM錯誤的風險。
  • 內存可以以頁爲單位立即回收,使那些需要使用較多內存的Java應用程序有足夠的內存可用。

併發壓縮是C4獨一無二的優勢。使應用程序線程GC線程協作運行,保證了應用程序不會因GC而被阻塞。C4將內存分配和提供足夠連續空閒內存的能力完全區分開。C4使你可以爲JVM實例分配儘可能大的內存,而無需爲應用程序暫停而煩惱。使用得當的話,這將是JVM技術的一項革新,它可以藉助於當今的多核、TB級內存的硬件優勢,大大提升低延遲Java應用程序的運行速度。

如果你不介意一遍又一遍的調優,以及頻繁的重啓的話,如果你的應用程序適用於水平部署模型的話(即部署幾百個小堆JVM實例而不是幾個大堆JVM實例),G1也是個不錯的選擇。

對於動態低延遲啓發性自適應(dynamic low-latency heuristic adaption)算法而言,BGC是一項革新,JVM研究者對此算法已經研究了幾十年。該算法可以應用於較大的堆。而動態自調優算法( dynamic self-tuning algorithm)的缺陷是,它無法跟上突然出現的負載高峯。那時,你將不得不面對最糟糕的場景,並根據實際情況再分配相關資源。

最後,爲你的應用程序選擇最適合的JVM和垃圾回收器時,最重要的考慮因素是應用程序中吞吐量和暫停時間的優先級次序。你想把時間和金錢花在哪?從純粹的技術角度說,基於我十年來對垃圾回收的經驗,我一直在尋找更多關於併發壓縮的革新性技術,或其他可以以較小代價完成移動對象或重定位的方法。我想影響企業級Java應用程序伸縮性的關鍵就在於併發性。

英文原文:JVM performance optimization, Part 4,翻譯:ImportNew - 曹旭東

譯文鏈接:http://www.importnew.com/2410.html

【如需轉載,請在正文中標註並保留原文鏈接、譯文鏈接和譯者等信息,謝謝合作!】

JVM 性能優化系列

第一篇 《JVM性能優化, Part 1 ―― JVM簡介 》

第二篇《JVM性能優化, Part 2 ―― 編譯器

第三篇《JVM性能優化, Part 3 —— 垃圾回收》

更多關於垃圾回收的文章

關於作者

Eva Andearsson對JVM計數、SOA、雲計算和其他企業級中間件解決方案有着10多年的從業經驗。在2001年,她以JRockit JVM開發者的身份加盟了創業公司Appeal Virtual Solutions(即BEA公司的前身)。在垃圾回收領域的研究和算法方面,EVA獲得了兩項專利。此外她還是提出了確定性垃圾回收(Deterministic Garbage Collection),後來形成了JRockit實時系統(JRockit Real Time)。在技術上,Eva與Sun公司和Intel公司合作密切,涉及到很多將JRockit產品線、WebLogic和Coherence整合的項目。2009年,Eva加盟了Azul System公司,擔任產品經理。負責新的Zing Java平臺的開發工作。最近,她改換門庭,以高級產品經理的身份加盟Cloudera公司,負責管理Cloudera公司Hadoop分佈式系統,致力於高擴展性、分佈式數據處理框架的開發。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章