Java虛擬機器04——垃圾收集器

llldddbbb發表於2019-04-07

主要介紹HotSpot虛擬機器的垃圾收集器,這個虛擬機器包含的所有收集器如圖所示:

image.png

可以看到,收集器之間是可以搭配使用的。下面介紹這些收集器的特性、基本原理和使用場景。在介紹之前先明確一個觀點:直到現在為止還沒有最好的收集器出現,更加沒有萬能的收集器,選擇的是對具體應用最合適的收集器。

序列收集器

序列收集器是最基本、發展歷史最悠久的收集器。它們的特點就是單執行緒執行及獨佔式執行,因此會帶來很不好的使用者體驗。雖然它的收集方式對程式的執行並不友好,但由於它的單執行緒執行特性,應用於單個CPU硬體平臺的效能可以超過其他的並行或併發處理器。

Serial收集器

  • 在JDk1.3之前是新生代收集的唯一選擇
  • 單執行緒,只會使用一個CPU去完成
  • 垃圾收集時,必須暫停其他工作執行緒,直到它收集結束。“Stop The World”
  • 虛擬機器後臺自動發起和自動完成的,在使用者不可見的情況下把使用者正常工作的執行緒全部停掉
  • 到現在為止,是虛擬機器執行在Client模式下的預設新生代收集器

通過JVM引數-XX:+UseSerialGC可以使用序列垃圾回收器。

Serial Old收集器

  • 是Serial收集器的老年代版本,是單執行緒收集器,採用“標記-整理演算法”
  • 主要意義也是在於給Client模式下的虛擬機器使用
  • 如果在Server模式下,那麼它主要還有兩大用途:
  •   在JDK 1.5以及之前的版本中與Parallel Scavenge收集器搭配使用;
  •   作為CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用。這兩點都將在後面的內容中詳細講解。

Serial與Serial Old工作過程如圖:

image.png

要啟用老年代序列收集器,可以嘗試使用下面的引數:

  • -XX:+UseSerialGC:新生代和老年代都是用序列收集器;
  • -XX:+UseParNewGC:新生代使用ParNew收集器,老年代使用序列收集器;
  • -XX:+UseParallelGC:新生代使用Parallel GC收集器,老年代使用序列收集器。

並行收集器

並行收集器是多執行緒的收集器,在多核CPU下能夠很好的提高收集效能。

ParNew收集器

ParNew收集器是Serial收集器的多執行緒版本。除了使用多條執行緒進行垃圾收集之外,其餘行為如所有控制引數(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集演算法、Stop The World、物件分配規則、回收策略等都與Serial收集器完全一樣,在實現上,這兩種收集器也共用了相當多的程式碼。ParNew收集器的工作過程如下圖所示:

image.png

  • 執行在Server模式下的虛擬機器中首選的新生代收集器,因為除了Serial收集器外,目前只有它能與CMS收集器配合工作
  • 由於存線上程互動的開銷,ParNew收集器在單CPU環境下效能並沒有單執行緒垃圾收集器效能好。
  • 可以使用-XX:ParallelGCThreads引數來限制垃圾收集的執行緒數。一般來說,當CPU數量小於8個時,宜設定為CPU數量;當CPU數量大於8個時,宜設定為3 + (( 5 * CPU_Count ) / 8 )。

開啟ParNew收集器可以使用以下引數:

  • -XX:+UseParNewGC:新生代使用ParNew收集器,老年代使用序列收集器。
  • -XX:+UseConcMarkSweepGC:新生代使用ParNew收集器,老年代使用CMS收集器。

ParNew是並行的收集器,在這裡介紹一下並行與併發的概念

  • 並行(Parallel):多條收集器執行緒並行工作,但此時使用者執行緒仍然處於等待狀態。(合作)
  • 併發(Concurrent):使用者執行緒與垃圾收集器執行緒同時執行,不一定是並行執行,可能是交替執行(競爭)

Parallel Scavenge收集器

Parallel Scavenge收集器與ParNew收集器類似,也是使用複製演算法的並行的多執行緒新生代收集器。但Parallel Scavenge收集器關注可控制的吞吐量(Throughput)

注:吞吐量是指CPU用於執行使用者程式碼的時間與CPU總消耗時間的比值,即吞吐量 = 執行使用者程式碼時間 /( 執行使用者程式碼時間 + 垃圾收集時間 )

Parallel Scavenge收集器提供了兩個引數用於精確控制吞吐量:

  • -XX:MaxGCPauseMillis:最大垃圾收集停頓時間,是一個大於0的毫秒數,收集器將回收時間儘量控制在這個設定值之內;但需要注意的是在同樣的情況下,回收時間與回收次數是成反比的,回收時間越小,相應的回收次數就會增多。所以這個值並不是越小越好。
  • -XX:GCTimeRatio:吞吐量大小,是一個(0, 100)之間的整數,表示垃圾收集時間佔總時間的比率。

除上述兩個引數之外,Parallel Scavenge收集器還提供了一個引數-XX:+UseAdaptiveSizePolicy,當這個引數開啟之後,就不需要手工指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRatio)、晉升老年代物件年齡(-XX:PretenureSizeThreshold)等細節引數了,虛擬機器會根據當前系統的執行情況收集效能監控資訊,動態調整這些引數以提供最合適的停頓時間或者最大的吞吐量,這種調節方式稱為GC自適應的調節策略(GC Ergonomics)。自適應調節策略也是Parallel Scavenge收集器與ParNew收集器的一個重要區別。

Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,使用多執行緒和“標記-整理”演算法。這個收集器是在JDK 1.6中才開始提供的。

由於如果新生代選擇了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器外別無選擇(Parallel Scavenge無法與CMS收集器配合工作),Parallel Old收集器的出現就是為了解決這個問題。Parallel Scavenge和Parallel Old收集器的組合更適用於注重吞吐量以及CPU資源敏感的場合。Parallel Old收集器的工作過程下圖所示:

image.png

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器,也是基於“標記—清除”演算法實現的,它的運作整個過程過程細分為4個步驟,包括:

  • 初始標記(CMS initial mark):需要“Stop The World”,標記一下GC Roots能直接關聯到的物件,速度很快。
  • 併發標記(CMS concurrent mark):進行GC Roots Tracing的過程,標記老年代所有存活物件。
  • 重新標記(CMS remark):需要“Stop The World”,修正併發標記期間因使用者程式繼續運作而導致標記產生變動的那一部分物件的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短。
  • 併發清除(CMS concurrent sweep):清理垃圾。

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

image.png

CMS有以下3個明顯的缺點:

  • CMS收集器對CPU資源非常敏感。CMS預設啟動的回收執行緒數是( CPU Count + 3 ) / 4,當CPU在4個以上時,併發回收時垃圾收集執行緒不少於25%的CPU資源,並且隨著CPU數量的增加而下降。但是當CPU不足4個(譬如2個)時,將分出一半的運算能力去執行收集器執行緒,就可能導致使用者程式的執行速度忽然降低了50%。

  • CMS收集器無法處理浮動垃圾(Floating Garbage),可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生。浮動垃圾是指CMS在併發清理階段使用者執行緒還在同時執行時產生的垃圾。由於在垃圾收集階段使用者執行緒還需要執行,還需要預留有足夠的記憶體空間給使用者執行緒使用,因此需要預留一部分空間提供併發收集時的程式運作使用。在JDK 1.5的預設設定下,CMS收集器當老年代使用了68%的空間後就會被啟用,可以適當調高引數-XX:CMSInitiatingOccupancyFraction的值來提高觸發百分比,以降低記憶體回收次數;在JDK 1.6中,CMS收集器的啟動閾值已經提升至92%。如果執行期間預留的記憶體無法滿足程式需要,就會出現一次“Concurrent Mode Failure”失敗,這時虛擬機器將臨時啟用Serial Old收集器來重新進行老年代的垃圾收集,導致停頓時間就很長了。所以說引數-XX:CMSInitiatingOccupancyFraction設定得太高很容易導致大量“Concurrent Mode Failure”失敗,效能反而降低。

  • CMS是一款基於“標記—清除”演算法實現的收集器,收集結束時會有大量空間碎片產生。空間碎片過多無法找到足夠大的連續空間來分配當前物件,不得不提前觸發一次Full GC。為了解決這個問題,CMS收集器提供了一個-XX:+UseCMSCompactAtFullCollection開關引數(預設就是開啟的),用於在CMS收集器要進行Full GC時開啟記憶體碎片的合併整理過程,記憶體整理的過程是無法併發的,因此也會導致停頓時間變長。而另外一個引數-XX:CMSFullGCsBeforeCompaction可以設定執行多少次不壓縮的Full GC後,才執行一次帶壓縮的(預設值為0,即每次進入Full GC時都進行碎片整理)。

G1收集器

G1是一款面向服務端應用的垃圾收集器,與其他GC收集器相比,G1具備如下特點:

  • 並行與併發:G1能充分利用多CPU、多核環境下的硬體優勢,使用多個CPU(CPU或者CPU核心)來縮短Stop-The-World停頓的時間。
  • 分代收集:分代概念在G1中依然保留。G1可以獨立管理整個GC堆,且採用不同的方式去處理分代物件。
  • 空間整合:G1從整體來看是基於“標記—整理”演算法實現的,從區域性(兩個Region之間)上來看是基於“複製”演算法實現的;G1收集後能提供規整的可用記憶體。 可預測的停頓:G1能建立可預測的停頓時間模型,能明確指定垃圾收集相對於時間段的吞吐量。
  • G1收集器將整個Java堆劃分為多個大小相等的獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們都是一部分Region(不需要連續)的集合。

G1根據各個Region回收所獲得的空間大小以及回收所需時間等指標在後臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region,從而可以有計劃地避免在整個Java堆中進行全區域的垃圾收集。

G1收集器運作步驟如下:

  • 初始標記(Initial Marking):僅僅只是標記一下GC Roots能直接關聯到的物件,速度很快,需要“Stop The World”。(OopMap)
  • 併發標記(Concurrent Marking): 進行GC Roots Tracing的過程
  • 最終標記(Final Marking): 修正併發標記期間因使用者程式繼續運作而導致標記產生變動的那一部分物件的標記記錄,這個階段的停頓時間一般會比初始標記階段稍長一些,但遠比並發標記的時間短,也需要“Stop The World”。(修正Remebered Set)
  • 篩選回收(Live Data Counting and Evacuation): 首先對各個Region的回收價值和成本進行排行,根據使用者所期望的GC停頓時間來制定回收計劃,這個階段其實也可以做到與使用者程式一起併發執行,但是因為只回收一部分Region,時間是使用者可控制的,而且停頓使用者執行緒將大幅提高收集效率。

image.png

垃圾收集引數總結

引數 描述
UseSerialGC 虛擬機器執行在Client模式下的預設值,開啟此開關後,使用 Serial+Serial Old 的收集器組合進行記憶體回收
UseParNewGC 開啟此開關後,使用 ParNew + Serial Old 的收集器組合進行記憶體回收
UseConcMarkSweepGC 開啟此開關後,使用 ParNew + CMS + Serial Old 的收集器組合進行記憶體回收。Serial Old 收集器將作為 CMS 收集器出現 Concurrent Mode Failure 失敗後的後備收集器使用
UseParallelGC 虛擬機器執行在 Server 模式下的預設值,開啟此開關後,使用 Parallel Scavenge + Serial Old(PS MarkSweep) 的收集器組合進行記憶體回收
UseParallelOldGC 開啟此開關後,使用 Parallel Scavenge + Parallel Old 的收集器組合進行記憶體回收
SurvivorRatio 新生代中 Eden 區域與 Survivor 區域的容量比值,預設為8,代表 Eden : Survivor = 8 : 1
PretenureSizeThreshold 直接晉升到老年代的物件大小,設定這個引數後,大於這個引數的物件將直接在老年代分配
MaxTenuringThreshold 晉升到老年代的物件年齡,每個物件在堅持過一次 Minor GC 之後,年齡就增加1,當超過這個引數值時就進入老年代
UseAdaptiveSizePolicy 動態調整 Java 堆中各個區域的大小以及進入老年代的年齡
HandlePromotionFailure 是否允許分配擔保失敗,即老年代的剩餘空間不足以應付新生代的整個 Eden 和 Survivor 區的所有物件都存活的極端情況
ParallelGCThreads 設定並行GC時進行記憶體回收的執行緒數
GCTimeRatio GC 時間佔總時間的比率,預設值為99,即允許 1% 的GC時間,僅在使用 Parallel Scavenge 收集器生效
MaxGCPauseMillis 設定 GC 的最大停頓時間,僅在使用 Parallel Scavenge 收集器時生效
CMSInitiatingOccupancyFraction 設定 CMS 收集器在老年代空間被使用多少後觸發垃圾收集,預設值為 68%,僅在使用 CMS 收集器時生效
UseCMSCompactAtFullCollection 設定 CMS 收集器在完成垃圾收集後是否要進行一次記憶體碎片整理,僅在使用 CMS 收集器時生效
CMSFullGCsBeforeCompaction 設定 CMS 收集器在進行若干次垃圾收集後再啟動一次記憶體碎片整理,僅在使用 CMS 收集器時生效

相關文章