JVM優化系列-JVM垃圾收集器介紹

導語
  既然是串行顧名思義就是使用單線程的方式進行執行,每次執行回收的時候,串行回收器只有一個工作線程,這樣對於並行能力較弱的計算機,串行回收器更多的獨佔線程專一執行的方面有着良好的實現,也就是說在單線程的程序中效果較好,可以在新生代和老年代,根據不同的堆空間氛圍新生代串行回收器和老年代串行回收器。下面就來分別談談這些收集器

串行回收器

新生代串行回收器

  串行收集器是所有的垃圾回收器中最爲古老的而一種收集器,在也是JVM最爲基本的垃圾回收器之一,它有兩個特點

  • 使用單線程進行垃圾回收
  • 獨佔式的垃圾回收器

  在串行收集器進行垃圾回收的時候,Java應用程序中的線程都是需要暫停的,在之前的分享中也看到過這個現象。在串行回收器運行的時候,應用程序的停止被稱爲是Stop-The-World。這種停頓如果時間太長的話就會影響用戶體驗,在對實時性操作要求較高的應用中這種往往是不能使用的。
在這裏插入圖片描述
  如上圖所示,串行收集器是一個經過長期生產環境的考驗,驗證爲極爲高效的垃圾收集器,在很多的場景中被使用。新生代串行處理器使用複製算法,實現相對比較簡單、邏輯處理高效、沒有線程之間切換的開銷。在很多的單CPU處理器等硬件平臺不是特別好的場景中,它的處理性能表現的比很多的並行處理器還要好。

  在之前的例子中使用瞭如下的參數

-XX:+UseSerialGC 這個參數可以指定使用新生代串行收集器和老年代串行收集器,當虛擬機在Client模式下進行運行的時候,它作爲其默認的垃圾收集器。在之前的博客中有對應的輸出實例這裏就不在過多的展示了

  注意 在查看垃圾收集器的效果的時候可以使用-XX:+PrintGCDetails 進行查看。

老年代串行回收器

  老年代串行收集器使用的是標記壓縮算法,與新生代的收集器一樣,也是一個串行、獨佔式的垃圾回收器。由於老年代垃圾回收的時間要比新生代的回收時間長,所以在堆空間比較大的應用中,一旦老年代的垃圾回收啓動之後,整個的應用程序可能會停頓很長的時間。影響用戶體驗。

  雖然是有很多的不好的地方,但是也是作爲老牌的垃圾收集器,老年代串行回收器和很多的新生代的垃圾回收器配合使用還是比較高效的,當然它也是CMS回收器的備用回收器。

  如果要想啓動老年代串行回收器,可以使用如下的一些參數

  • -XX:+UseSerialGC:新生代、老年代都使用串行回收器
  • -XX:+UseParNewGC: 新生代使用ParNew回收器,老年代使用串行收集器
  • -XX:+UseParallelGC:新生代使用ParallelGC回收器,老年代使用串行收集器

並行回收器

  並行回收器是在串行回收器的基礎上做了改進,可以使用多個線程同時進行垃圾回收看上去要不單線程收集的速度要快,但是如果底層的硬件不能有效的支持這個並行處理的能力也就不會體現出其垃圾回收的價值。不過在性能支持的場景下這種收集器可以有效的縮短垃圾回收的時間。

新生代ParNew回收器

  ParNew 回收器作爲一個工作在新生代的垃圾收集器,只能簡單地將串行回收器進行多線程處理,它的回收策略、算法以及參數和新生代串行回收器一樣。

在這裏插入圖片描述

  如上圖ParNew回收器也是獨佔式的回收器,在收集過程中,應用程序會全部暫停。但由於並行回收器使用多線程進行垃圾回收,所以在併發能力較強的CPU上,所產生的停頓時間要短與串行回收器,在單CPU或者併發能力不是太好的系統中,並行回收器就不會比串行收集器好,由於多線程的壓力,可能性能上比單線程所表現出的效果還要差。

  開啓ParNew回收器可以使用如下的一些參數

  • -XX:+UseParNewGC: 新生代使用ParNew回收器,老年代使用串行回收器
  • -XX:+UseConcMarkSweepGC:新生代使用ParNew回收器,老年代使用CMS

  ParNew回收器工作時的線程數量可以使用-XX:ParallelGCThreads參數指定。在一般情況下最好是與CPU的數量相同,避免線程過多的線程數,從而影響到整個的垃圾收集的性能,在默認情況下,當CPU數量小於8個,ParallelGCThreads的值等於CPU數量,當CPU數量大於8個的時候,ParallelGCThreads 的值等於3+((5*CPU_Count)/8)

-Xmx1g -Xms1g -Xmn900m -XX:+UseParNewGC -Xloggc:gc.log -XX:+PrintGCDetails

第一次 ParNew回收器的日誌輸入信息

[GC (Allocation Failure) [ParNew: 737280K->28149K(829440K), 0.0276526 secs] 737280K->28149K(956416K), 0.0276925 secs] [Times: user=0.31 sys=0.02, real=0.03 secs] 

  可以看到,整個輸出的結果和新生代串行收集器是一樣的,只有回收器標識符不同。

新生代ParallelGC回收器

  新生代ParallelGC回收器也是使用複製算法的收集器,從表面上來看,和ParNew回收器是一樣的,都是多線程獨佔式的收集器。ParallelGC回收器有一個重要的特點就是關注與系統的吞吐量。

  新生代ParallelGC回收器可以使用一下參數啓動

  • -XX:+UseParallelGC: 新生代使用ParallelGC回收器,老年代使用串行回收器
  • -XX:+UseParallelGCOldGC:新生代使用ParallelGC回收器,老年代使用ParallelOldGC回收器。

ParallelGC回收器提供了兩個重要參數用於控制系統的吞吐量。

  • -XX:MaxGCPauseMillis: 設置最大垃圾收集停頓時間,它的值是一個大於0 的整數。

  在ParallelGC工作的時候,會調整Java堆大小或者是其他的一些參數,儘可能的把停頓時間控制在MaxGCPauseMillis以內。但是如果使用者希望可以減少停頓的時間,而把這個值變得很小,爲了達到預期的停頓時間,虛擬機可能會使用一個較小的堆,而這個將導致垃圾回收變得頻繁,從而增加垃圾回收的總時間。

  • -XX:GCTimeRatio : 設置吞吐量大小,它的值是一個0到100 之間的整數。

  如果GCTimeRatio 的值設爲n,那麼系統將花費不超過1/(1+n)的時間用於垃圾收集。

  ParallelGC回收器與ParNew回收器的另一個不同之處在於它還支持一種自適應的GC調節策略。使用-XX:+UseAdaptiveSizePolicy可以打開自適應GC策略。在這種模式下,新生代的大小、eden和survivor 的比例、晉升老年代的對象年齡等參數都會被自動調整,以達到在堆大小、吞吐量和停頓時間之間的平衡點。在手工調優困難的場景下可以直接使用這種自適應的方式,僅指定虛擬機的最大堆、目標吞吐量(GCTimeRatio)和停頓時間(MaxGCPauseMillis),讓虛擬機自己完成調優。

-Xmx1g -Xms1g -Xmn900m -XX:+UseParallelGC -Xloggc:gc.log -XX:+PrintGCDetails

ParallelGC 回收器的工作日誌如下

14.341: [GC (Allocation Failure) --[PSYoungGen: 691200K->691200K(806400K)] 691200K->818176K(933376K), 0.7543450 secs] [Times: user=1.15 sys=5.70, real=0.75 secs] 

  顯示ParallelGC 回收器的工作成果,也就是回收前的內存大小和回收後的內存大小以及花費的時間。

老年代ParallelOldGC回收器

  老年代ParallelOldGC 回收器也是一種多線程併發收集器。與新生代ParallelGC回收器一樣,也是一個關注吞吐量的收集器,它是一種老年代的垃圾回收器,並且可以與新生代的ParallelGC進行搭配使用。

  ParallelOldGC 回收器使用標記壓縮算法,在JDK1.6 的時候纔開始使用。
在這裏插入圖片描述

  使用如下的參數進行操作

  • -XX:+UseParallelOldGC 可以在新生代使用ParallelGC 回收器,老年代使用 ParallelOldGC回收器。在對吞吐量敏感的系統中可以考慮使用
  • -XX:ParallelGCThreads 也可以用於設置垃圾回收時的線程數量
15.007: [Full GC (Ergonomics) [PSYoungGen: 691200K->459974K(806400K)] [ParOldGen: 126976K->126676K(126976K)] 818176K->586650K(933376K), [Metaspace: 3146K->3146K(1056768K)], 0.4226251 secs] [Times: user=4.57 sys=0.11, real=0.42 secs] 

  從日誌中可以看到它顯示了新生代、老年代以及元數據區的使用情況。以及Full GC所消耗的時間。

CMS 回收器

  與之前的垃圾回收器不同,CMS回收器主要關注與系統停頓的時間。CMS是使用Concurrent Mark Sweep 的縮寫。也就是併發標記清除,從名稱上就可以看到。使用標記清除算法,同時使用併發多線程垃圾回收器。

CMS 主要工作步驟

  CMS回收器的工作過程與其他垃圾收集器相比較,略顯複雜。在CMS工作時,主要的步驟有如下幾個:初始標記、併發標記、預清理、重新標記、併發清除和併發重置。其中初始標記和重新標記是獨佔系統資源的,而預清理、併發標記、併發清除和併發重置是可以和用戶線程一起執行。所以,從整體上,CSM收集不是獨佔式的,它可以在應用程序運行過程中進行垃圾回收。CMS流程如下:
在這裏插入圖片描述
  根據標記清除算法,初始標記、併發標記和重新標記都是爲了標記出需要回收的對象。併發清理則是在標記完成後,正式回收垃圾對象。併發重置是指在垃圾回收完成後,重新初始化CMS數據結構和數據,爲下一次垃圾回收做好準備。併發標記、併發清理等併發重置都是可以和應用程序線程一起執行。

  在整個的CMS的執行回收過程中,默認在併發標記之後,會有一個預清理的操作(-XX:-CMSPrecleaningEnable)。預清理是併發的,除了爲正式清理做準備和檢查之外,預清理還會嘗試控制一次停頓的時間。由於重新標記是獨佔CPU的,如果新生代GC發生後,立即觸發一次重新標記。這次停頓的時間可能會很長,所以在預處理的時候,會等待到下一次的新生代GC的發生,然後根據歷史性能數據預測下一次新生代GC可能發生的時間,然後在當前時間與預測時間的中間時刻,進行重新的標記,這樣可以避免了新生代GC和重新標記重合,儘可能減少停頓時間。

CMS主要的參數設置

  • -XX:+UseConcMarkSweepGC :表示啓動對應的CMS垃圾收集器

  CMS默認的啓動併發線程數(ParallelGCThreads+3)/4),ParallelGCThreads 表示 GC並行時使用的線程數量,如果新生代使用ParNew,那麼ParallelGCThreads 也就是新生代GC的線程數量也就是是4個,之後1個併發線程,而且兩個併發線程是有5~8 個ParallelGCThreads線程數。

  併發線程數量也可以通過如下的兩個參數進行設定

  • -XX:ConcGCThreads
  • -XX:ParallelCMSThreads

  當CPU資源資源不足的時候,CMS回收線程會導致應用系統性能下降。

注意
  併發是指收集器和應用程序交替執行,並行是指應用程序停止,同時由多個線程一起執行GC。因此並行收集器並不是併發的。因爲並行回收器執行時,應用程序完全掛起,不存在交替執行的步驟。

  由於CMS回收器不是獨佔式的回收器,在CMS執行的過程中還會有應用程序仍然在工作,又會產生新的垃圾。這些新的垃圾在CMS回收過程中是無法清除的,同時應用程序沒有中斷,所以在CMS執行過程中,還應該確保應用程序有足夠的內存可用。所以,CMS回收器不會等地堆內存飽和的時候才進行回收操作,而是當內存使用率達到某個閾值的時候開始進行回收,這樣可以保證CMS在工作過程中依然有足夠的空間支持應用程序運行。

  之前提到的回收閾值可以通過 -XX:CMSInitiatingOccupancyFraction 來進行指定,默認是68。也就是說當老年代空間使用率達到68%,會執行一次CMS回收。如果應用程序的內存使用率增長很快,在CMS在執行過程中,已經出現了內存不足就會導致CMS回收就會失敗,虛擬機將啓動老年代串行收集器進行垃圾回收,如果這樣應用程序將完全中斷,直到垃圾回收完成,這時候應用程序會停頓很長的時間。

調優
  根據應用程序的特點,可以對 -XX:CMSInitiatingOccupancyFraction 進行調優。如果內存增長緩慢,則可以設置一個較大的值,大的閾值可以有效降低CMS的觸發頻率。

  CMS 是一個基於標記清除算法的回收器。導致內存大量的碎片,在這種情況下即使堆內存仍然有較大的剩餘空間,也可能就會被迫進行一次垃圾回收,以換取一塊可以使用的連續內存。爲了解決這個問題CMS提供了幾個壓縮整理參數。

  • -XX:+UseCMSCompactAtFullCollection :表示可以在CMS進行垃圾回收之後進行一次內存碎片整理,內存碎片的整理並不是併發執行的
  • -XX:CMSFullGCsBeforeCompaction 參數可以用於設定進行多少次CMS回收之後,進行一次內存壓縮。

CMS的日誌分析

-Xmx1g -Xms1g -Xmn900m  -XX:+UseConcMarkSweepGC -Xloggc:gc.log -XX:+PrintGCDetails

GC日誌結果

14.979: [GC (Allocation Failure) 14.979: [ParNew: 737280K->43307K(829440K), 0.0238308 secs] 737280K->43307K(956416K), 0.0239861 secs] [Times: user=0.26 sys=0.04, real=0.02 secs] 
32.211: [GC (Allocation Failure) 32.211: [ParNew: 780587K->92160K(829440K), 0.1395256 secs] 780587K->190880K(956416K), 0.1396500 secs] [Times: user=0.48 sys=0.29, real=0.14 secs] 
## CMS 標記初始化
32.351: [GC (CMS Initial Mark) [1 CMS-initial-mark: 98720K(126976K)] 209792K(956416K), 0.0094776 secs] [Times: user=0.03 sys=0.00, real=0.01 secs] 

## 併發標記開始
32.361: [CMS-concurrent-mark-start]
32.370: [CMS-concurrent-mark: 0.009/0.009 secs] [Times: user=0.04 sys=0.00, real=0.01 secs]

## 併發預清理開始
32.370: [CMS-concurrent-preclean-start]
## 預清理
32.370: [CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

## 併發終止預清理
32.370: [CMS-concurrent-abortable-preclean-start]
32.370: [CMS-concurrent-abortable-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

## CMS 最終標記
32.370: [GC (CMS Final Remark) [YG occupancy: 125817 K (829440 K)]32.370: [Rescan (parallel) , 0.0104235 secs]32.381: [weak refs processing, 0.0000152 secs]32.381: [class unloading, 0.0003597 secs]32.381: [scrub symbol table, 0.0003096 secs]32.382: [scrub string table, 0.0001029 secs][1 CMS-remark: 98720K(126976K)] 224538K(956416K), 0.0113683 secs] [Times: user=0.13 sys=0.01, real=0.01 secs] 
## 並行掃描 
32.382: [CMS-concurrent-sweep-start]
32.395: [CMS-concurrent-sweep: 0.013/0.013 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
## 並行重置
32.395: [CMS-concurrent-reset-start]
32.395: [CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

  通過上面的日誌可以看出完全按照之前的幾個步驟來進行執行的,只不過具體的堆內存大小分析還需要讀者自己查看
  通過上面的輸入結果可以看到,整個的GC操作日誌結構與之前都有所不同。這裏需要測試一個參數,就是-XX:CMSInitiatingOccupancyFraction參數。

-Xmx1g -Xms1g -Xmn900m  -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=30 -Xloggc:gc.log -XX:+PrintGCDetails
15.010: [GC (Allocation Failure) 15.010: [ParNew: 737280K->43258K(829440K), 0.0278249 secs] 737280K->43258K(956416K), 0.0279809 secs] [Times: user=0.27 sys=0.05, real=0.03 secs] 
32.130: [GC (Allocation Failure) 32.130: [ParNew: 780538K->92160K(829440K), 0.1360428 secs] 780538K->190882K(956416K), 0.1361398 secs] [Times: user=0.48 sys=0.30, real=0.14 secs] 
32.266: [GC (CMS Initial Mark) [1 CMS-initial-mark: 98722K(126976K)] 209794K(956416K), 0.0093485 secs] [Times: user=0.03 sys=0.00, real=0.01 secs] 
32.276: [CMS-concurrent-mark-start]
32.283: [CMS-concurrent-mark: 0.008/0.008 secs] [Times: user=0.03 sys=0.00, real=0.01 secs] 
32.283: [CMS-concurrent-preclean-start]
32.284: [CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
32.284: [CMS-concurrent-abortable-preclean-start]
32.284: [CMS-concurrent-abortable-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
32.284: [GC (CMS Final Remark) [YG occupancy: 125817 K (829440 K)]32.284: [Rescan (parallel) , 0.0088066 secs]32.293: [weak refs processing, 0.0000120 secs]32.293: [class unloading, 0.0002377 secs]32.293: [scrub symbol table, 0.0002658 secs]32.293: [scrub string table, 0.0000923 secs][1 CMS-remark: 98722K(126976K)] 224539K(956416K), 0.0095009 secs] [Times: user=0.11 sys=0.00, real=0.01 secs] 
32.293: [CMS-concurrent-sweep-start]
32.310: [CMS-concurrent-sweep: 0.016/0.016 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
32.310: [CMS-concurrent-reset-start]
32.310: [CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

 如果下GC日誌裏發現併發收集失敗,這可能就是由於應用程序在運行過程中老年代的空間不足導致,如果在CMS工作中出現非常頻繁的併發模式失敗,就應該考慮調整,就可以通過上面這個參數來降低CMS觸發的閾值,使得CMS在執行過程中,仍然會有較大的老年代空間可以使用

-Xmx1g -Xms1g -Xmn900m  -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=90 -Xloggc:gc.log -XX:+PrintGCDetails
002651 secs]42.969: [scrub symbol table, 0.0003033 secs]42.969: [scrub string table, 0.0001144 secs][1 CMS-remark: 98725K(126976K)] 648111K(956416K), 0.0155712 secs] [Times: user=0.19 s
ys=0.00, real=0.02 secs] 
42.969: [CMS-concurrent-sweep-start]
43.012: [CMS-concurrent-sweep: 0.043/0.043 secs] [Times: user=0.05 sys=0.00, real=0.04 secs] 
43.013: [CMS-concurrent-reset-start]
43.013: [CMS-concurrent-reset: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
45.017: [GC (CMS Initial Mark) [1 CMS-initial-mark: 3503K(126976K)] 637603K(956416K), 0.0165192 secs] [Times: user=0.20 sys=0.01, real=0.01 secs] 
45.034: [CMS-concurrent-mark-start]
45.034: [CMS-concurrent-mark: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
45.034: [CMS-concurrent-preclean-start]
45.035: [CMS-concurrent-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
45.035: [CMS-concurrent-abortable-preclean-start]
49.579: [GC (Allocation Failure) 49.579: [ParNew (promotion failed): 829440K->829440K(829440K), 0.3785727 secs]49.958: [CMS49.997: [CMS-concurrent-abortable-preclean: 2.310/4.963 secs] 
[Times: user=3.65 sys=0.22, real=4.97 secs] 
 (concurrent mode failure): 126140K->126976K(126976K), 0.3357808 secs] 832943K->321892K(956416K), [Metaspace: 3171K->3171K(1056768K)], 0.7145068 secs] [Times: user=1.53 sys=0.16, real=0.71 secs] 
52.293: [GC (CMS Initial Mark) [1 CMS-initial-mark: 126976K(126976K)] 424421K(956416K), 0.0436691 secs] [Times: user=0.07 sys=0.00, real=0.05 secs] 

關於Class的回收

  在使用CMS回收器時,如果需要回收Perm區,那麼就會觸發一次FullGC,但是如果希望使用CMS回收Perm區,必須要進行-XX:+CMSClassUnloadingEnabled 進行設置使用了這個參數,如果條件允許,系統會使用CMS的回收Perm區Class數據。

總結

  到這裏傳統的在1.7之前的垃圾回收器都說完了,在後面的分享中會提到一個新的垃圾回收器-G1,敬請期待吧!

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