深入理解java虛擬機3

經典垃圾收集器

收集算法是內存回收的方法論,垃圾收集器就是內存回收的實踐者。


如果兩個收集器之間存在連線,就說明它們可以搭配使用,圖中收集器所處的區域,則表示它是屬於新生代收集器抑或是老年代收集器。


Serial收集器

一個單線程工作的收集器,並不僅僅是說明它只會使用一個處理器或一條收集線程去完成垃圾收集工作,更重要的是強調在它進行垃圾收集時,必須暫停其他所有工作線程,直到它收集結束。

ParNew收集器

Serial收集器的多線程並行版本,除了同時使用多條線程進行垃圾收集之外,其餘的行爲包括Serial收集器可用的所有控制參數(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、對象分配規則、回收策略等都與Serial收集器完全一致。

·並行(Parallel):並行描述的是多條垃圾收集器線程之間的關係,說明同一時間有多條這樣的線程在協同工作,通常默認此時用戶線程是處於等待狀態。

·併發(Concurrent):併發描述的是垃圾收集器線程與用戶線程之間的關係,說明同一時間垃圾收集器線程與用戶線程都在運行。由於用戶線程並未被凍結,所以程序仍然能響應服務請求,但由於垃圾收集器線程佔用了一部分系統資源,此時應用程序的處理的吞吐量將受到一定影響。

除了Serial收集器外,目前只有它能與CMS收集器配合工作。它默認開啓的收集線程數與處理器核心數量相同,在處理器核心非常多(譬如32個)的環境中,可以使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。


Parallel Scavenge收集器

一款新生代收集器,基於標記-複製算法實現的收集器,也是能夠並行收集的多線程收集器。

Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量(Throughput)。所謂吞吐量就是處理器用於運行用戶代碼的時間與處理器總消耗時間的比值,主要適合在後臺運算而不需要太多交互的分析任務。

Parallel Scavenge收集器提供了兩個參數用於精確控制吞吐量,分別是控制最大垃圾收集停頓時間的-XX:MaxGCPauseMillis參數以及直接設置吞吐量大小的-XX:GCTimeRatio參數。

-XX:MaxGCPauseMillis參數允許的值是一個大於0的毫秒數。

-XX:GCTimeRatio參數的值則應當是一個大於0小於100的整數,也就是垃圾收集時間佔總時間的比率,相當於吞吐量的倒數。默認值爲99,即允許最大1%(即1/(1+99))的垃圾收集時間。

-XX:+UseAdaptiveSizePolicy是一個開關參數,當這個參數被激活之後,就不需要人工指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRatio)、晉升老年代對象大小(-XX:PretenureSizeThreshold)等細節參數了,虛擬機會根據當前系統的運行情況收集性能監控信息,動態調整這些參數以提供最合適的停頓時間或者最大的吞吐量。

自適應調節策略也是Parallel Scavenge收集器區別於ParNew收集器的一個重要特性。


Serial Old收集器

Serial收集器的老年代版本,它同樣是一個單線程收集器,使用標記-整理算法。


Parallel Old收集器

Parallel Scavenge收集器的老年代版本,支持多線程併發收集,基於標記-整理算法實現。

在注重吞吐量或者處理器資源較爲稀缺的場合,都可以優先考慮Parallel Scavenge加Parallel Old收集器這個組合。


CMS收集器

基於標記-清除算法實現的,一種以獲取最短回收停頓時間爲目標的收集器。

運作過程相對於前面幾種收集器來說要更復雜一些,整個過程分爲四個步驟,包括:

1)初始標記(CMS initial mark)

2)併發標記(CMS concurrent mark)

3)重新標記(CMS remark)

4)併發清除(CMS concurrent sweep)

初始標記、重新標記這兩個步驟仍然需要“Stop The World”。初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快;

併發標記階段就是從GC Roots的直接關聯對象開始遍歷整個對象圖的過程,這個過程耗時較長但是不需要停頓用戶線程,可以與垃圾收集線程一起併發運行;

重新標記階段則是爲了修正併發標記期間,因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄(增量更新),這個階段的停頓時間通常會比初始標記階段稍長一些,但也遠比並發標記階段的時間短;

併發清除階段,清理刪除掉標記階段判斷的已經死亡的對象,由於不需要移動存活對象,所以這個階段也是可以與用戶線程同時併發的。

由於在整個過程中耗時最長的併發標記和併發清除階段中,垃圾收集器線程都可以與用戶線程一起工作,所以從總體上來說,CMS收集器的內存回收過程是與用戶線程一起併發執行的。

CMS收集器有以下三個缺點

1. CMS收集器對處理器資源非常敏感。在併發階段,它雖然不會導致用戶線程停頓,但卻會因爲佔用了一部分處理器的計算能力而導致應用程序變慢,降低總吞吐量。CMS默認啓動的回收線程數是(處理器核心數量+3)/4,也就是說,當處理器核心數量不足四個時,CMS對用戶程序的影響就可能變得很大。

2. 由於CMS收集器無法處理“浮動垃圾”,有可能出現“Con-current Mode Failure”失敗進而導致另一次完全“Stop The World”的Full GC的產生。在CMS的併發標記和併發清理階段,用戶線程是還在繼續運行的,程序在運行自然就還會伴隨有新的垃圾對象不斷產生,但這一部分垃圾對象是出現在標記過程結束以後,CMS無法在當次收集中處理掉它們,只好留待下一次垃圾收集時再清理掉。這一部分垃圾就稱爲“浮動垃圾”。

由於在垃圾收集階段用戶線程還需要持續運行,那就還需要預留足夠內存空間提供給用戶線程使用,因此CMS收集器不能像其他收集器那樣等待到老年代幾乎完全被填滿了再進行收集,必須預留一部分空間供併發收集時的程序運作使用,如果在實際應用中老年代增長並不是太快,可以適當調高參數-XX:CMSInitiatingOccu-pancyFraction的值來提高CMS的觸發百分比,降低內存回收頻率,獲取更好的性能。要是CMS運行期間預留的內存無法滿足程序分配新對象的需要,就會出現一次“併發失敗”(Concurrent Mode Failure),這時候虛擬機將不得不啓動後備預案:凍結用戶線程的執行,臨時啓用Serial Old收集器來重新進行老年代的垃圾收集,但這樣停頓時間就很長了。所以參數-XX:CMSInitiatingOccupancyFraction設置得太高將會很容易導致大量的併發失敗產生,性能反而降低。

3. CMS是一款基於“標記-清除”算法實現的收集器,這意味着會有空間碎片產生。空間碎片過多時,將會給大對象分配帶來很大麻煩,往往會出現老年代還有很多剩餘空間,但就是無法找到足夠大的連續空間來分配對象,而不得不提前觸發一次Full GC的情況。(爲了解決這個問題,CMS收集器提供了一個-XX:+UseCMS-CompactAtFullCollection開關參數(默認是開啓的,此參數從JDK 9開始廢棄),用於在CMS收集器不得不進行Full GC時開啓內存碎片的合併整理過程,由於這個內存整理必須移動存活對象,是無法併發的。這樣停頓時間又會變長,因此虛擬機設計者們還提供了另外一個參數-XX:CMSFullGCsBefore-Compaction(此參數從JDK 9開始廢棄),這個參數的作用是要求CMS收集器在執行過若干次(數量由參數值決定)不整理空間的Full GC之後,下一次進入Full GC前會先進行碎片整理(默認值爲0,表示每次進入Full GC時都進行碎片整理)。)


Garbage First收集器

G1是一款主要面向服務端應用的垃圾收集器。到了JDK 8 Update 40版本以後的G1收集器才被Oracle官方稱爲“全功能的垃圾收集器”(Fully-Featured Garbage Collector)。目標是替代CMS收集器。

面向堆內存任何部分來組成回收集進行回收,標準是: 哪塊內存中存放的垃圾數量最多,回收收益最大,這就是G1收集器的Mixed GC模式。而不追求一次把整個Java堆全部清理乾淨。

G1不再堅持傳統的固定大小以及固定數量的分代區域劃分,而是把連續的Java堆劃分爲多個大小相等的獨立區域(Region),每一個Region都可以根據需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間。

收集器能夠對扮演不同角色的Region採用不同的策略去處理,這樣無論是新創建的對象還是已經存活了一段時間、熬過多次收集的舊對象都能獲取很好的收集效果。

Region中還有一類特殊的Humongous區域,專門用來存儲大對象。G1認爲只要大小超過了一個Region容量一半的對象即可判定爲大對象。每個Region的大小可以通過參數-XX:G1HeapRegionSize設定,取值範圍爲1MB~32MB,且應爲2的N次冪。而對於那些超過了整個Region容量的超級大對象,將會被存放在N個連續的Humongous Region之中,G1的大多數行爲都把Humongous Region作爲老年代的一部分來進行看待。

G1收集器可以跟蹤各個Region裏面的垃圾堆積的“價值”大小,價值即回收所獲得的空間大小以及回收所需時間的經驗值,然後在後臺維護一個優先級列表,每次根據用戶設定允許的收集停頓時間(使用參數-XX:MaxGCPauseMillis指定,默認值是200毫秒),優先處理回收價值收益最大的那些Region,這也就是“Garbage First”名字的由來。

這種使用Region劃分內存空間,以及具有優先級的區域回收方式,保證了G1收集器在有限的時間內獲取儘可能高的收集效率。

G1收集器的運作過程大致可劃分爲以下四個步驟:

·初始標記(Initial Marking):僅僅只是標記一下GC Roots能直接關聯到的對象,並且修改TAMS指針的值,讓下一階段用戶線程併發運行時,能正確地在可用的Region中分配新對象。這個階段需要停頓線程,但耗時很短,而且是借用進行Minor GC的時候同步完成的,所以G1收集器在這個階段實際並沒有額外的停頓。

·併發標記(Concurrent Marking):從GC Root開始對堆中對象進行可達性分析,遞歸掃描整個堆裏的對象圖,找出要回收的對象,這階段耗時較長,但可與用戶程序併發執行。當對象圖掃描完成以後,還要重新處理SATB記錄下的在併發時有引用變動的對象。

·最終標記(Final Marking):對用戶線程做另一個短暫的暫停,用於處理併發階段結束後仍遺留下來的最後那少量的SATB記錄。

·篩選回收(Live Data Counting and Evacuation):負責更新Region的統計數據,對各個Region的回收價值和成本進行排序,根據用戶所期望的停頓時間來制定回收計劃,可以自由選擇任意多個Region構成回收集,然後把決定回收的那一部分Region的存活對象複製到空的Region中,再清理掉整個舊Region的全部空間。這裏的操作涉及存活對象的移動,是必須暫停用戶線程,由多條收集器線程並行完成的。

除了併發標記外,其餘階段也是要完全暫停用戶線程的,換言之,它並非純粹地追求低延遲,官方給它設定的目標是在延遲可控的情況下獲得儘可能高的吞吐量,所以才能擔當起“全功能收集器”的重任與期望。

可以由用戶指定期望的停頓時間是G1收集器很強大的一個功能,這裏設置的“期望值”必須是符合實際的,它默認的停頓目標爲兩百毫秒,一般來說,回收階段佔到幾十到一百甚至接近兩百毫秒都很正常,

但如果我們把停頓時間調得非常低,如設置爲二十毫秒,很可能出現的結果就是由於停頓目標時間太短,導致每次選出來的回收集只佔堆內存很小的一部分,收集器收集的速度逐漸跟不上分配器分配的速度,導致垃圾慢慢堆積,最終佔滿堆引發Full GC反而降低性能,所以通常把期望停頓時間設置爲一兩百毫秒或者兩三百毫秒會是比較合理的。

缺點:

用戶程序運行過程中,G1無論是爲了垃圾收集產生的內存佔用(Footprint)還是程序運行時的額外執行負載(Overload)都要比CMS要高。

內存佔用來說,雖然G1和CMS都使用卡表來處理跨代指針,但G1的卡表實現更爲複雜,而且堆中每個Region,無論扮演的是新生代還是老年代角色,都必須有一份卡表,這導致G1的記憶集可能會佔整個堆容量的20%乃至更多的內存空間;相比起來CMS的卡表就相當簡單,只有唯一一份,而且只需要處理老年代到新生代的引用,反過來則不需要,由於新生代的對象具有朝生夕滅的不穩定性,引用變化頻繁,能省下這個區域的維護開銷是很划算的。

執行負載的角度上,同樣由於兩個收集器各自的細節實現特點導致了用戶程序運行時的負載會有不同,比如它們都使用到寫屏障,CMS用寫後屏障來更新維護卡表,而G1除了使用寫後屏障來進行同樣的卡表維護操作外,爲了實現原始快照搜索(SATB)算法,還需要使用寫前屏障來跟蹤併發時的指針變化情況。相比起增量更新算法,原始快照搜索能夠減少併發標記和重新標記階段的消耗,避免CMS那樣在最終標記階段停頓時間過長的缺點,但是在用戶程序運行過程中確實會產生由跟蹤引用變化帶來的額外負擔。此外, 由於G1對寫屏障的複雜操作要比CMS消耗更多的運算資源,所以CMS的寫屏障實現是直接的同步操作,而G1就不得不將其實現爲類似於消息隊列的結構,把寫前屏障和寫後屏障中要做的事情都放到隊列裏,然後再異步處理。

目前在小內存應用上CMS的表現大概率仍然要會優於G1,而在大內存應用上G1則大多能發揮其優勢,這個優劣勢的Java堆容量平衡點通常在6GB至8GB之間,當然,不同應用需要實際測試才能得出最合適的結論。

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