C4 垃圾回收

使用C4垃圾回收器能夠有效提高對低延遲有要求的企業級Java應用程序的伸縮性。html

到目前爲止,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算法將釋放內存的過程從應用程序行爲和內存分配速率中分離出來,並加以區分。這樣就實現了併發運行,即應用程序能夠持續運行,而沒必要等待垃圾回收的完成。其中的併發性是關鍵所在,正是因爲併發性的存在纔可使暫停時間不受垃圾回收週期內堆上活動數據數量和須要跟蹤與更新的引用數量的影響,將暫停時間保持在較低的水平。大多數垃圾回收器在工做週期內都包含了stop-the-world式的壓縮過程,這就是說應用程序的暫停時間會隨活動數據總量和堆中對象間引用的複雜度的上升而增長。使用C4算法的垃圾回收器能夠併發的執行壓縮操做,即壓縮與應用程序線程同時工做,從而解決了影響JVM伸縮性的最大難題。數據結構

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

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

C4算法的3個階段併發

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

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

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

C4算法中的標記階段

在C4算法中,標記階段(marking phase)使用了併發標記(concurrent marking)和引用跟蹤(reference-tracing)的方法來標記活動對象,這方面內容能夠參照通常的垃圾回收算法。

在標記階段中,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應用程序訪問內存的傳統限制。

通過上述的過程後,如何更新引用呢?如何實現一個非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)的缺陷是,它沒法跟上忽然出現的負載高峯。那時,你將不得不面對最糟糕的場景,並根據實際狀況再分配相關資源。

相關文章
相關標籤/搜索