深入理解Java虛擬機器筆記之四關於垃圾收集器

zhumeilu發表於2019-01-18

深入理解Java虛擬機器筆記之四關於垃圾收集器

Serial收集器

這個收集器是一個單執行緒的收集器,但它的“單執行緒”的意義並不僅僅說明它只會使用一個CPU或一條收集執行緒去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作執行緒,直到它收集結束。

“Stop The World”,是由虛擬機器在後臺自動發起和自動完成的,在使用者不可見的情況下把使用者正常工作的執行緒全部停掉。

深入理解Java虛擬機器筆記之四關於垃圾收集器

Serial收集器是虛擬機器執行在Client模式下的預設新生代收集器。

ParNew收集器

ParNew收集器其實就是Serial收集器的多執行緒版本,除了使用多執行緒進行垃圾收集之外,其餘行為包括Serial收集器可用的所有控制引數、收集演算法、Stop The World、物件分配規則、回收策略等都與Serial收集器完全一樣。

深入理解Java虛擬機器筆記之四關於垃圾收集器

ParNew收集器除了多執行緒收集之外,其他與Serial收集器相比沒有太多創新之處,但它卻是許多執行在Server模式下的虛擬機器中首選的新生代收集器,其中有一個與效能無關但很重要的原因是,除了Serial收集器外,目前只有他能與CMS收集器配合工作。ParNew收集器是使用-XX:+UseConcMarkSweepGC選項後的預設新生收集器,也可以使用-XX:+UseParNewGC選項來強制指定它。

ParNew收集器在單CPU的環境中,沒有Serial收集器效果好。當然隨著可以使用的CPU的數量的增加,它對應GC時系統資源的有效利用還是很有好處的。它預設開啟的收集執行緒數與CPU的數量相同,在CPU非常多的環境下,可以使用-XX:ParallelGCThreads引數來限制垃圾收集的執行緒數。

Parallel Scavenge收集器

Parallel Scavenge收集器是一個新生代收集器,它也是使用複製演算法的收集器,又是並行的多執行緒收集器。

Parallel Scavenge收集器的特點是它的關注點與其他收集器不同,CMS等收集器的關注點是儘可能的縮短垃圾收集時使用者執行緒的停頓時間,而Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量。

所謂吞吐量就是CPU用於執行使用者程式碼的時間與CPU總消耗時間的比值,即吞吐量 = 執行使用者程式碼時間/(執行使用者程式碼時間+垃圾收集時間),虛擬機器總共執行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。

停頓時間越短就越適合需要與使用者互動的程式,良好的響應速度能提升使用者體驗,而高吞吐量則可以高效率的利用CPU時間,儘快完成程式的運算任務,主要適合在後臺運算而不需要太多互動的任務。

Parallel Scavenge收集器提供了兩個引數用於精確控制吞吐量,控制最大垃圾收集停頓時間的-XX:MaxGCPauseMillis,吞吐量大小-XX:GCTimeRatio。

MaxGCPauseMillis引數允許的值是一個大於0的毫秒數,收集器將盡可能的保證記憶體回收花費的時間不超過設定值。不過大家不要認為如果把這個引數的值設定的稍小一點就能使得系統的垃圾收集速度變得更快,GC停頓時間縮短是以犧牲吞吐量和新生代空間來換取的:系統把新生代調小一些,收集300MB新生代肯定比收集500MB快吧,這也直接導致垃圾收集發生的更頻繁一些,原來10秒收集一次、每次停頓100ms,現在變成5秒收集一次、每次停頓70ms。停頓時間的確在下降,但吞吐量也降下來了。

GCTimeRatio引數的值應當是一個大於0且小於100的整數,也就是垃圾收集時間佔總時間的比率,相當於是吞吐量的倒數。預設值為99,就是允許最大1%(即1/(1+99))的垃圾收集時間。

由於與吞吐量關係密切,Parallel Scavenge收集器收集器也經常稱為“吞吐量優先”收集器。 自適應調節策略:-XX:+UseAdaptiveSizePolicy。這是一個開關引數,當這個引數開啟之後,就不需要手工指定新生代的大小、Eden與Survivor區的比例、晉升老年代物件大小(+XX:PretenureSizeThreshold)等細節引數了,虛擬機器會根據當前系統的執行情況收集效能監控資訊,動態調整這些引數以提供最合適的停頓時間或者最大的吞吐量。只需要把基本的記憶體資料設定好(如-Xmx設定最大堆),然後使用MaxGCPauseMillis引數(更關注最大停頓時間)或GCTimeRatio(更關注吞吐量)引數給虛擬機器設定一個優化目標,具體細節引數的調節工作就由虛擬機器完成了。

Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,它同樣是一個單執行緒收集器。這個收集器的主要意義也是在於給Client模式下的虛擬機器使用。在Server模式下主要有兩大用途:JDK1.5以及之前的版本中與Parallel Scavenge收集器搭配使用。作為CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用。

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多執行緒和“標記-整理”演算法。

深入理解Java虛擬機器筆記之四關於垃圾收集器

CMS收集器

HotSpot虛擬機器中第一款真正意義上的併發收集器,第一次實現了讓垃圾收集執行緒與使用者執行緒(基本上)同時工作。 CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。

CMS收集器是基於“標記-清除”演算法實現的,運作過程分為4個步驟:

  • 初始標記
  • 併發標記
  • 重新標記
  • 併發清除

其中,初始標記、重新標記仍然需要“Stop The World”。初始化標記僅僅只是標記一下GC Roots 能直接關聯到的物件,速度很快,併發標記階段就是進行GC Roots Tracing的過程,而重新標記階段則是為了修正併發標記期間因使用者程式繼續運作而導致標記產生變動的那一部分物件的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短。

由於整個過程中耗時最長的併發標記和併發清除過程收集器執行緒都可以與使用者執行緒一起工作,所以,總體上來講,CMS收集器的記憶體回收過程是與使用者執行緒一起併發執行的。

深入理解Java虛擬機器筆記之四關於垃圾收集器

三個明顯的缺點:

  • CMS收集器對CPU資源非常敏感。

CMS預設啟動的回收執行緒數是(CPU數量+3)/4,也就是當CPU在4個以上時,併發回收時垃圾收集執行緒不少於25%的CPU資源,並且隨著CPU數量的增加而下降。但是當CPU不足4個時,CMS對使用者程式的影響就可能變的很大,如果本來CPU負載就比較大,還分出一半的運算能力去執行收集器執行緒,就可能導致使用者程式的執行速度忽然降低了50%,讓人無法接受。

  • CMS收集器無法處理浮動垃圾,可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生。

由於CMS併發清理階段使用者執行緒還在執行著,伴隨程式執行自然就還會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之後,CMS無法在當次收集中處理掉它們,只好留待下一次GC時在清理掉。這一部分垃圾就稱為“浮動垃圾”。也是由於在垃圾收集階段使用者執行緒還需要執行,那也就還需要預留有足夠的記憶體空間給使用者執行緒使用,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿了再進行收集,需要預留一部分空間提供併發收集時的程式運作使用。

可以通過設定-XX:CMSInitiatingOccupancyFraction來提高觸發百分比,以便降低記憶體回收次數從而獲得更好的效能。要是CMS執行期預留的記憶體無法滿足程式需要,就會出現一次“Concurrent Mode Failure”失敗,這時虛擬機器將啟動後備方案:臨時啟用Serial Old收集器來重新進行老年代的垃圾收集,這樣停頓的時間就很長了。

  • CMS基於“標記-清除”演算法實現,在垃圾收集結束時會有大量空間碎片產生

空間碎片過多時將會給大物件分配帶來很大麻煩,往往會出現老年代還有很大空間剩餘,但是無法找到足夠大的連續空間來分配當前物件,不得不提前觸發一次Full GC。為了解決這個問題,CMS收集器提供了一個-XX:+UseCMSCompactAtFullCollection開關引數(預設開啟),用於在CMS收集器頂不住要進行Full GC時開啟記憶體碎片的合併整理過程,記憶體整理的過程是無法併發的,空間碎片問題沒有了,但停頓時間不得不變長。虛擬機器設計者還提供了另外一個引數-XX:CMSFullGCsBeforeCompaction,這個引數是用於設定執行多少次不壓縮的Full GC後,跟著來一次帶壓縮的(預設值為0,表示每次進入Full GC是都進行碎片整理)。

G1收集器

G1是一款面向服務端應用的垃圾收集器。它的使命是未來可以替換掉CMS收集器。

特點:

  • 並行與併發

    G1能充分利用多CPU、多核環境下的硬體優勢,使用多個CPU來縮短Stop The World停頓的時間,部分其他收集器原本需要停頓Java執行緒執行的GC操作,G1收集器仍然可以通過併發的方式讓Java程式繼續執行。

  • 分代收集

    與其他收集器一樣,分代概念在G1中仍然得以保留。雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但它能夠採用不同的方式去處理新建立的物件和已經存活了一段時間、熬過多次GC的舊物件以獲取更好的收集效果。

  • 空間整合

    與CMS的“標記-清理”演算法不同,G1從整體來看是基於“標記-整理”演算法實現的收集器,從區域性(兩個Region之間)上來看是基於“複製”演算法實現的,但無論如何,這兩種演算法都意味著G1運作期間不會產生記憶體空間碎片,收集後能提供整的可用記憶體。這種特性有利於程式長時間執行,分配大物件時不會因為無法找到連續記憶體空間而提前觸發下一次GC。

  • 可預測的停頓

    這是G1相對於CMS的另一大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已經是實時Java的垃圾收集器的特徵了。

使用G1收集器時,Java堆的記憶體佈局與其他收集器有很大區別,它將整個Java堆劃分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續)的集合。

G1收集器之所以能建立可預測的停頓時間模型,是因為它可以有計劃地避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裡面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據運營的收集時間,優先回收價值最大的Region(這也就是Garbage-First名稱的來由)。這種使用Region劃分記憶體空間以及有優先順序的區域回收方式,保證了G1收集器在有限的時間內可以獲取儘可能高的收集效率。

在G1收集器中,Region之間的物件引用以及其他收集器中的新生代與老年代之間的物件引用,虛擬機器都是使用Remembered Set來避免全堆掃描的。G1中每個Region都有一個與之對應的Remembered Set,虛擬機器發現程式在對Reference型別的資料進行寫操作時,會產生一個Write Barrier暫時中斷寫操作,檢查Reference引用的物件是否處於不同的Region之中(在分代的例子中就是檢查是否老年代中的物件引用了新生代中的物件),如果是,便通過CardTable把相關引用資訊記錄到被引用物件所屬的Region的Remembered Set之中。當進行記憶體回收時,在GC根節點的列舉範圍中加入Remembered Set即可保證不對全棧掃描也不會有遺漏。

運作步驟:

  • 初始標記

    標記一下GC Roots能直接關聯到的物件,並修改TAMS(Next Top at Mark Start)的值,讓下一階段使用者程式併發執行時,能在正確可用的Region中建立新物件,這階段需要停頓執行緒,但耗時很短。

  • 併發標記

    從GC Root開始對堆中物件進行可達性分析,找出存活的物件,這階段耗時較長,但可以與使用者程式併發執行

  • 最終標記

    最終標記階段則是為了修正在併發標記期間因使用者程式繼續運作二導致標記產生變動的那一部分標記記錄,虛擬機器將這段時間物件變化記錄線上程Remembered Set Logs裡面,最終標記階段需要把Remembered Set Logs的資料合併到Remembered Set中,這階段需要停頓執行緒,但是可以併發執行。

  • 篩選回收

    最後在篩選回收階段首先對各個Region的回收價值和成本進行排序,根據使用者所期望的GC停頓時間來指定回收計劃。

深入理解Java虛擬機器筆記之四關於垃圾收集器

相關文章