HotSpot JVM的併發標記清理收集器(CMS收集器)的主要目標就是:下降低應用停頓時間。該目標對於大多數交互式應用很重要,好比web應用。web
就像吞吐量收集器,CMS收集器處理老年代的對象,然而其操做要複雜得多。吞吐量收集器老是暫停應用程序線程,而且多是至關長的一段時間,然而這可以使該算法安全地忽略應用程序。相比之下,CMS收集器被設計成在大多數時間能與應用程序線程並行執行,僅僅會有一點(短暫的)停頓時間。GC與應用程序並行的缺點就是,可能會出現各類同步和數據不一致的問題。爲了實現安全且正確的併發執行,CMS收集器的GC週期被分爲了好幾個連續的階段。算法
CMS收集器的GC週期由6個階段組成。其中4個階段(名字以Concurrent開始的)與實際的應用程序是併發執行的,而其餘2個階段須要暫停應用程序線程。安全
初始標記:爲了收集應用程序的對象引用須要暫停應用程序線程,該階段完成後,應用程序線程再次啓動。服務器
併發標記:從第一階段收集到的對象引用開始,遍歷全部其餘的對象引用。多線程
併發預清理:改變當運行第二階段時,由應用程序線程產生的對象引用,以更新第二階段的結果。併發
從新標記:因爲第三階段是併發的,對象引用可能會發生進一步改變。所以,應用程序線程會再一次被暫停以更新這些變化,而且在進行實際的清理以前確保一個正確的對象引用視圖。這一階段十分重要,由於必須避免收集到仍被引用的對象。性能
併發清理:全部再也不被應用的對象將從堆裏清除掉。測試
併發重置:收集器作一些收尾的工做,以便下一次GC週期能有一個乾淨的狀態。線程
一個常見的誤解是,CMS收集器運行是徹底與應用程序併發的。咱們已經看到,事實並不是如此,即便「stop-the-world」階段相對於併發階段的時間很短。設計
應該指出,儘管CMS收集器爲老年代垃圾回收提供了幾乎徹底併發的解決方案,然而年輕代仍然經過「stop-the-world」方法來進行收集。對於交互式應用,停頓也是可接受的,背後的原理是年輕帶的垃圾回收時間一般是至關短的。
當咱們在真實的應用中使用CMS收集器時,咱們會面臨兩個主要的挑戰,可能須要進行調優:
堆碎片是有可能的,不像吞吐量收集器,CMS收集器並無任何碎片整理的機制。所以,應用程序有可能出現這樣的情形,即便總的堆大小遠沒有耗盡,但卻不能分配對象——僅僅是由於沒有足夠連續的空間徹底容納對象。當這種事發生後,併發算法不會幫上任何忙,所以,萬不得已JVM會觸發Full GC。回想一下,Full GC 將運行吞吐量收集器的算法,從而解決碎片問題——但卻暫停了應用程序線程。所以儘管CMS收集器帶來徹底的併發性,但仍然有可能發生長時間的「stop-the-world」的風險。這是「設計」,而不能避免的——咱們只能經過調優收集器來它的可能性。想要100%保證避免」stop-the-world」,對於交互式應用是有問題的。
第二個挑戰就是應用的對象分配率高。若是生產對象實例的頻率高於收集器清除堆裏死對象的頻率,併發算法將再次失敗。從某種程度上說,老年代將沒有足夠的可用空間來容納一個從年輕代提高過來的對象。這種狀況被稱爲「併發模式失敗」,而且JVM會執行堆碎片整理:觸發Full GC。
當這些情形之一出如今實踐中時(常常會出如今生產系統中),常常被證明是老年代有大量沒必要要的對象。一個可行的辦法就是增長年輕代的堆大小,以防止年輕代短生命的對象提早進入老年代。另外一個辦法就彷佛利用分析器,快照運行系統的堆轉儲,而且分析過分的對象分配,找出這些對象,最終減小這些對象的申請。
下面我看看大多數與CMS收集器調優相關的JVM標誌參數。
該標誌首先是激活CMS收集器。默認HotSpot JVM使用的是並行收集器。
當使用CMS收集器時,該標誌激活年輕代使用多線程並行執行垃圾回收。這使人很驚訝,咱們不能簡單在並行收集器中重用-XX:UserParNewGC標誌,由於概念上年輕代用的算法是同樣的。然而,對於CMS收集器,年輕代GC算法和老年代GC算法是不一樣的,所以年輕代GC有兩種不一樣的實現,而且是兩個不一樣的標誌。
注意最新的JVM版本,當使用-XX:+UseConcMarkSweepGC時,-XX:UseParNewGC會自動開啓。所以,若是年輕代的並行GC不想開啓,能夠經過設置-XX:-UseParNewGC來關掉。
當該標誌被啓用時,併發的CMS階段將以多線程執行(所以,多個GC線程會與全部的應用程序線程並行工做)。該標誌已經默認開啓,若是順序執行更好,這取決於所使用的硬件,多線程執行能夠經過-XX:-CMSConcurremntMTEnabled禁用。
標誌-XX:ConcGCThreads=(早期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調優。
當堆滿以後,並行收集器便開始進行垃圾收集,例如,當沒有足夠的空間來容納新分配或提高的對象。對於CMS收集器,長時間等待是不可取的,由於在併發垃圾收集期間應用持續在運行(而且分配對象)。所以,爲了在應用程序使用完內存以前完成垃圾收集週期,CMS收集器要比並行收集器更先啓動。
由於不一樣的應用會有不一樣對象分配模式,JVM會收集實際的對象分配(和釋放)的運行時數據,而且分析這些數據,來決定何時啓動一次CMS垃圾收集週期。爲了引導這一過程, JVM會在一開始執行CMS週期前做一些線索查找。該線索由 -XX:CMSInitiatingOccupancyFraction=來設置,該值表明老年代堆空間的使用率。好比,value=75意味着第一次CMS垃圾收集會在老年代被佔用75%時被觸發。一般CMSInitiatingOccupancyFraction的默認值爲68(以前很長時間的經歷菜決定的)。
咱們用-XX+UseCMSInitiatingOccupancyOnly標誌來命令JVM不基於運行時收集的數據來啓動CMS垃圾收集週期。而是,當該標誌被開啓時,JVM經過CMSInitiatingOccupancyFraction的值進行每一次CMS收集,而不只僅是第一次。然而,請記住大多數狀況下,JVM比咱們本身能做出更好的垃圾收集決策。所以,只有當咱們充足的理由(好比測試)而且對應用程序產生的對象的生命週期有深入的認知時,才應該使用該標誌。
相對於並行收集器,CMS收集器默認不會對永久代進行垃圾回收。若是但願對永久代進行垃圾回收,可用設置標誌-XX:+CMSClassUnloadingEnabled。在早期JVM版本中,要求設置額外的標誌-XX:+CMSPermGenSweepingEnabled。注意,即便沒有設置這個標誌,一旦永久代耗盡空間也會嘗試進行垃圾回收,可是收集不會是並行的,而再一次進行Full GC。
該標誌將開啓CMS收集器的增量模式。增量模式常常暫停CMS過程,以便對應用程序線程做出徹底的讓步。所以,收集器將花更長的時間完成整個收集週期。所以,只有經過測試後發現正常CMS週期對應用程序線程干擾太大時,才應該使用增量模式。因爲現代服務器有足夠的處理器來適應併發的垃圾收集,因此這種狀況發生得不多。
現在,被普遍接受的最佳實踐是避免顯式地調用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標誌的機會,該標誌將告訴JVM徹底忽略系統的GC調用(無論使用的收集器是什麼類型)。對於我而言,該標誌屬於默認的標誌集合中,能夠安全地定義在每一個JVM上運行,而不須要進一步思考。