1. 程式人生 > >JVM實用引數(七)CMS收集器

JVM實用引數(七)CMS收集器

原文連線 本文連線  譯者: iDestiny  校對:樑海艦

HotSpot JVM的併發標記清理收集器(CMS收集器)的主要目標就是:低應用停頓時間。該目標對於大多數互動式應用很重要,比如web應用。在我們看一下有關JVM的引數之前,讓我們簡要回顧CMS收集器的操作和使用它時可能出現的主要挑戰。

就像吞吐量收集器(參見本系列的第6部分),CMS收集器處理老年代的物件,然而其操作要複雜得多。吞吐量收集器總是暫停應用程式執行緒,並且可能是相當長的一段時間,然而這能夠使該演算法安全地忽略應用程式。相比之下,CMS收集器被設計成在大多數時間能與應用程式執行緒並行執行,僅僅會有一點(短暫的)停頓時間。GC與應用程式並行的缺點就是,可能會出現各種同步和資料不一致的問題。為了實現安全且正確的併發執行,CMS收集器的GC週期被分為了好幾個連續的階段。

CMS收集器的過程

CMS收集器的GC週期由6個階段組成。其中4個階段(名字以Concurrent開始的)與實際的應用程式是併發執行的,而其他2個階段需要暫停應用程式執行緒。

  1. 初始標記:為了收集應用程式的物件引用需要暫停應用程式執行緒,該階段完成後,應用程式執行緒再次啟動。
  2. 併發標記:從第一階段收集到的物件引用開始,遍歷所有其他的物件引用。
  3. 併發預清理:改變當執行第二階段時,由應用程式執行緒產生的物件引用,以更新第二階段的結果。
  4. 重標記:由於第三階段是併發的,物件引用可能會發生進一步改變。因此,應用程式執行緒會再一次被暫停以更新這些變化,並且在進行實際的清理之前確保一個正確的物件引用檢視。這一階段十分重要,因為必須避免收集到仍被引用的物件。
  5. 併發清理:所有不再被應用的物件將從堆裡清除掉。
  6. 併發重置:收集器做一些收尾的工作,以便下一次GC週期能有一個乾淨的狀態。

一個常見的誤解是,CMS收集器執行是完全與應用程式併發的。我們已經看到,事實並非如此,即使“stop-the-world”階段相對於併發階段的時間很短。

應該指出,儘管CMS收集器為老年代垃圾回收提供了幾乎完全併發的解決方案,然而年輕代仍然通過“stop-the-world”方法來進行收集。對於互動式應用,停頓也是可接受的,背後的原理是年輕帶的垃圾回收時間通常是相當短的。

挑戰

當我們在真實的應用中使用CMS收集器時,我們會面臨兩個主要的挑戰,可能需要進行調優:

  1. 堆碎片
  2. 物件分配率高

堆碎片是有可能的,不像吞吐量收集器,CMS收集器並沒有任何碎片整理的機制。因此,應用程式有可能出現這樣的情形,即使總的堆大小遠沒有耗盡,但卻不能分配物件——僅僅是因為沒有足夠連續的空間完全容納物件。當這種事發生後,併發演算法不會幫上任何忙,因此,萬不得已JVM會觸發Full GC。回想一下,Full GC 將執行吞吐量收集器的演算法,從而解決碎片問題——但卻暫停了應用程式執行緒。因此儘管CMS收集器帶來完全的併發性,但仍然有可能發生長時間的“stop-the-world”的風險。這是“設計”,而不能避免的——我們只能通過調優收集器來它的可能性。想要100%保證避免”stop-the-world”,對於互動式應用是有問題的。

第二個挑戰就是應用的物件分配率高。如果獲取物件例項的頻率高於收集器清除堆裡死物件的頻率,併發演算法將再次失敗。從某種程度上說,老年代將沒有足夠的可用空間來容納一個從年輕代提升過來的物件。這種情況被稱為“併發模式失敗”,並且JVM會執行堆碎片整理:觸發Full GC。

當這些情形之一出現在實踐中時(經常會出現在生產系統中),經常被證實是老年代有大量不必要的物件。一個可行的辦法就是增加年輕代的堆大小,以防止年輕代短生命的物件提前進入老年代。另一個辦法就似乎利用分析器,快照執行系統的堆轉儲,並且分析過度的物件分配,找出這些物件,最終減少這些物件的申請。

下面我看看大多數與CMS收集器調優相關的JVM標誌引數。

-XX:+UseConcMarkSweepGC

該標誌首先是啟用CMS收集器。預設HotSpot JVM使用的是並行收集器。

-XX:UseParNewGC

當使用CMS收集器時,該標誌啟用年輕代使用多執行緒並行執行垃圾回收。這令人很驚訝,我們不能簡單在並行收集器中重用-XX:UserParNewGC標誌,因為概念上年輕代用的演算法是一樣的。然而,對於CMS收集器,年輕代GC演算法和老年代GC演算法是不同的,因此年輕代GC有兩種不同的實現,並且是兩個不同的標誌。

注意最新的JVM版本,當使用-XX:+UseConcMarkSweepGC時,-XX:UseParNewGC會自動開啟。因此,如果年輕代的並行GC不想開啟,可以通過設定-XX:-UseParNewGC來關掉。

-XX:+CMSConcurrentMTEnabled

當該標誌被啟用時,併發的CMS階段將以多執行緒執行(因此,多個GC執行緒會與所有的應用程式執行緒並行工作)。該標誌已經預設開啟,如果順序執行更好,這取決於所使用的硬體,多執行緒執行可以通過-XX:-CMSConcurremntMTEnabled禁用。

 -XX:ConcGCThreads

標誌-XX:ConcGCThreads=<value>(早期JVM版本也叫-XX:ParallelCMSThreads)定義併發CMS過程執行時的執行緒數。比如value=4意味著CMS週期的所有階段都以4個執行緒來執行。儘管更多的執行緒會加快併發CMS過程,但其也會帶來額外的同步開銷。因此,對於特定的應用程式,應該通過測試來判斷增加CMS執行緒數是否真的能夠帶來效能的提升。

如果還標誌未設定,JVM會根據並行收集器中的-XX:ParallelGCThreads引數的值來計算出預設的並行CMS執行緒數。該公式是ConcGCThreads = (ParallelGCThreads + 3)/4。因此,對於CMS收集器, -XX:ParallelGCThreads標誌不僅影響“stop-the-world”垃圾收集階段,還影響併發階段。

總之,有不少方法可以配置CMS收集器的多執行緒執行。正是由於這個原因,建議第一次執行CMS收集器時使用其預設設定, 然後如果需要調優再進行測試。只有在生產系統中測量(或類生產測試系統)發現應用程式的暫停時間的目標沒有達到 , 就可以通過這些標誌應該進行GC調優。

-XX:CMSInitiatingOccupancyFraction

當堆滿之後,並行收集器便開始進行垃圾收集,例如,當沒有足夠的空間來容納新分配或提升的物件。對於CMS收集器,長時間等待是不可取的,因為在併發垃圾收集期間應用持續在執行(並且分配物件)。因此,為了在應用程式使用完記憶體之前完成垃圾收集週期,CMS收集器要比並行收集器更先啟動。

因為不同的應用會有不同物件分配模式,JVM會收集實際的物件分配(和釋放)的執行時資料,並且分析這些資料,來決定什麼時候啟動一次CMS垃圾收集週期。為了引導這一過程, JVM會在一開始執行CMS週期前作一些線索查詢。該線索由-XX:CMSInitiatingOccupancyFraction=<value>來設定,該值代表老年代堆空間的使用率。比如,value=75意味著第一次CMS垃圾收集會在老年代被佔用75%時被觸發。通常CMSInitiatingOccupancyFraction的預設值為68(之前很長時間的經歷來決定的)。

-XX:+UseCMSInitiatingOccupancyOnly

我們用-XX+UseCMSInitiatingOccupancyOnly標誌來命令JVM不基於執行時收集的資料來啟動CMS垃圾收集週期。而是,當該標誌被開啟時,JVM通過CMSInitiatingOccupancyFraction的值進行每一次CMS收集,而不僅僅是第一次。然而,請記住大多數情況下,JVM比我們自己能作出更好的垃圾收集決策。因此,只有當我們充足的理由(比如測試)並且對應用程式產生的物件的生命週期有深刻的認知時,才應該使用該標誌。

-XX:+CMSClassUnloadingEnabled

相對於並行收集器,CMS收集器預設不會對永久代進行垃圾回收。如果希望對永久代進行垃圾回收,可用設定標誌-XX:+CMSClassUnloadingEnabled。在早期JVM版本中,要求設定額外的標誌-XX:+CMSPermGenSweepingEnabled。注意,即使沒有設定這個標誌,一旦永久代耗盡空間也會嘗試進行垃圾回收,但是收集不會是並行的,而再一次進行Full GC。

-XX:+CMSIncrementalMode

該標誌將開啟CMS收集器的增量模式。增量模式經常暫停CMS過程,以便對應用程式執行緒作出完全的讓步。因此,收集器將花更長的時間完成整個收集週期。因此,只有通過測試後發現正常CMS週期對應用程式執行緒干擾太大時,才應該使用增量模式。由於現代伺服器有足夠的處理器來適應併發的垃圾收集,所以這種情況發生得很少。

-XX:+ExplicitGCInvokesConcurrent and -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses

如今,被廣泛接受的最佳實踐是避免顯式地呼叫GC(所謂的“系統GC”),即在應用程式中呼叫system.gc()。然而,這個建議是不管使用的GC演算法的,值得一提的是,當使用CMS收集器時,系統GC將是一件很不幸的事,因為它預設會觸發一次Full GC。幸運的是,有一種方式可以改變預設設定。標誌-XX:+ExplicitGCInvokesConcurrent命令JVM無論什麼時候呼叫系統GC,都執行CMS GC,而不是Full GC。第二個標誌-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses保證當有系統GC呼叫時,永久代也被包括進CMS垃圾回收的範圍內。因此,通過使用這些標誌,我們可以防止出現意料之外的”stop-the-world”的系統GC。

-XX:+DisableExplicitGC

然而在這個問題上…這是一個很好提到- XX:+ DisableExplicitGC標誌的機會,該標誌將告訴JVM完全忽略系統的GC呼叫(不管使用的收集器是什麼型別)。對於我而言,該標誌屬於預設的標誌集合中,可以安全地定義在每個JVM上執行,而不需要進一步思考。