Java GC性能優化實戰

GC優化是必要的嗎?

或者更準確地說,GC優化對Java基礎服務來講是必要的嗎?答案是否認的,事實上GC優化對Java基礎服務來講在有些場合是能夠省去的,但前提是這些正在運行的Java系統,必須包含如下參數或行爲: 
+ 內存大小已經經過-Xms和-Xmx參數指定過 
+ 運行在server模式下(使用-server參數) 
+ 系統中沒有殘留超時日誌之類的錯誤日誌git

換句話說,若是你在運行時沒有手動設置內存大小而且打印出了過多的超時日誌,那你就須要對系統進行GC優化。github

不過你須要時刻謹記一句話:GC tuning is the last task to be done.算法

如今來想想GC優化的最根本緣由,垃圾收集器的工做就是清除Java建立的對象,垃圾收集器須要清理的對象數量以及要執行的GC數量均取決於已建立的對象數量。所以,爲了使你的系統在GC上表現良好,首先須要減小建立對象的數量。數據庫

俗話說「冰凍三尺非一日之寒」,咱們在編碼時要首先要把下面這些小細節作好,不然一些瑣碎的不良代碼累積起來將讓GC的工做變得繁重而難於管理:服務器

  • 使用StringBuilderStringBuffer來代替String
  • 儘可能少輸出日誌

儘管如此,仍然會有咱們一籌莫展的狀況。XML和JSON解析過程每每佔用了最多的內存,即便咱們已經儘量地少用String、少輸出日誌,仍然會有大量的臨時內存(大約10-100MB)被用來解析XML或JSON文件,但咱們又很難棄用XML和JSON。在此,你只須要知道這一過程會佔據大量內存便可。併發

若是在通過幾回重複的優化後應用程序的內存用量狀況有所改善,那麼久能夠啓動GC優化了。jvm

筆者總結了GC優化的兩個目的: 
1. 將進入老年代的對象數量降到最低 
2. 減小Full GC的執行時間工具

將進入老年代的對象數量降到最低

除了能夠在JDK 7及更高版本中使用的G1收集器之外,其餘分代GC都是由Oracle JVM提供的。關於分代GC,就是對象在Eden區被建立,隨後被轉移到Survivor區,在此以後剩餘的對象會被轉入老年代。也有一些對象因爲佔用內存過大,在Eden區被建立後會直接被傳入老年代。老年代GC相對來講會比新生代GC更耗時,所以,減小進入老年代的對象數量能夠顯著下降Full GC的頻率。你可能會覺得減小進入老年代的對象數量意味着把它們留在新生代,事實正好相反,新生代內存的大小是能夠調節的。性能

下降Full GC的時間

Full GC的執行時間比Minor GC要長不少,所以,若是在Full GC上花費過多的時間(超過1s),將可能出現超時錯誤。 
+ 若是經過減少老年代內存來減小Full GC時間,可能會引發OutOfMemoryError或者致使Full GC的頻率升高。 
+ 另外,若是經過增長老年代內存來下降Full GC的頻率,Full GC的時間可能所以增長。測試

所以,你須要把老年代的大小設置成一個「合適」的值。

影響GC性能的參數

不要幻想着「若是有人用他設置的GC參數獲取了不錯的性能,咱們爲何不復制他的參數設置呢?」,由於對於不用的Web服務,它們建立的對象大小和生命週期都不相同。

舉一個簡單的例子,若是一個任務的執行條件是A,B,C,D和E,另外一個徹底相同的任務執行條件只有A和B,那麼哪個任務執行速度更快呢?做爲常識來說,答案很明顯是後者。

Java GC參數的設置也是這個道理,設置好幾個參數並不會提高GC執行的速度,反而會使它變得更慢。GC優化的基本原則是將不一樣的GC參數應用到兩個及以上的服務器上而後比較它們的性能,而後將那些被證實能夠提升性能或減小GC執行時間的參數應用於最終的工做服務器上。

下面這張表展現了與內存大小相關且會影響GC性能的GC參數

 

表1:GC優化須要考慮的JVM參數

 

類型 參數 描述
堆內存大小 -Xms 啓動JVM時堆內存的大小
  -Xmx 堆內存最大限制
新生代空間大小 -XX:NewRatio 新生代和老年代的內存比
  -XX:NewSize 新生代內存大小
  -XX:SurvivorRatio Eden區和Survivor區的內存比

筆者在進行GC優化時最經常使用的參數是-Xms,-Xmx-XX:NewRatio-Xms-Xmx參數一般是必須的,因此NewRatio的值將對GC性能產生重要的影響。

有些人可能會問如何設置永久代內存大小,你能夠用-XX:PermSize-XX:MaxPermSize參數來進行設置,可是要記住,只有當出現OutOfMemoryError錯誤時你才須要去設置永久代內存。

還有一個會影響GC性能的因素是垃圾收集器的類型,下表展現了關於GC類型的可選參數(基於JDK 6.0):

 

表2:GC類型可選參數

 

GC類型 參數 備註
Serial GC -XX:+UseSerialGC  
Parallel GC -XX:+UseParallelGC-XX:ParallelGCThreads=value  
Parallel Compacting GC -XX:+UseParallelOldGC  
CMS GC -XX:+UseConcMarkSweepGC-XX:+UseParNewGC-XX:+CMSParallelRemarkEnabled-XX:CMSInitiatingOccupancyFraction=value-XX:+UseCMSInitiatingOccupancyOnly  
G1 -XX:+UnlockExperimentalVMOptions-XX:+UseG1GC 在JDK 6中這兩個參數必須配合使用

除了G1收集器外,能夠經過設置上表中每種類型第一行的參數來切換GC類型,最多見的非侵入式GC就是Serial GC,它針對客戶端系統進行了特別的優化。

會影響GC性能的參數還有不少,可是上述的參數會帶來最顯著的效果,請切記,設置太多的參數並不必定會提高GC的性能。

GC優化的過程

GC優化的過程和大多數常見的提高性能的過程類似,下面是筆者使用的流程:

1.監控GC狀態

你須要監控GC從而檢查系統中運行的GC的各類狀態

2.分析監控結果後決定是否須要優化GC

在檢查GC狀態後,你須要分析監控結構並決定是否須要進行GC優化。若是分析結果顯示運行GC的時間只有0.1-0.3秒,那麼就不須要把時間浪費在GC優化上,但若是運行GC的時間達到1-3秒,甚至大於10秒,那麼GC優化將是頗有必要的。

可是,若是你已經分配了大約10GB內存給Java,而且這些內存沒法省下,那麼就沒法進行GC優化了。在進行GC優化以前,你須要考慮爲何你須要分配這麼大的內存空間,若是你分配了1GB或2GB大小的內存而且出現了OutOfMemoryError,那你就應該執行堆轉儲(heap dump)來消除致使異常的緣由。

注意:

堆轉儲(heap dump)是一個用來檢查Java內存中的對象和數據的內存文件。該文件能夠經過執行JDK中的jmap命令來建立。在建立文件的過程當中,全部Java程序都將暫停,所以,不要再系統執行過程當中建立該文件。

你能夠在互聯網上搜索heap dump的詳細說明。

3.設置GC類型/內存大小

若是你決定要進行GC優化,那麼你須要選擇一個GC類型而且爲它設置內存大小。此時若是你有多個服務器,請如上文提到的那樣,在每臺機器上設置不一樣的GC參數並分析它們的區別。

4.分析結果

在設置完GC參數後就能夠開始收集數據,請在收集至少24小時後再進行結果分析。若是你足夠幸運,你可能會找到系統的最佳GC參數。如若否則,你還須要分析輸出日誌並檢查分配的內存,而後須要經過不斷調整GC類型/內存大小來找到系統的最佳參數。

5.若是結果使人滿意,將參數應用到全部服務器上並結束GC優化

若是GC優化的結果使人滿意,就能夠將參數應用到全部服務器上,並中止GC優化。

在下面的章節中,你將會看到上述每一步所作的具體工做。

監控GC狀態並分析結果

在運行中的Web應用服務器(Web Application Server,WAS)上查看GC狀態的最佳方式就是使用jstat命令。

下面的例子展現了某個尚未執行GC優化的JVM的狀態(雖然它並非運行服務器)。

$ jstat -gcutil 21719 1s S0 S1 E O P YGC YGCT FGC FGCT GCT 48.66 0.00 48.10 49.70 77.45 3428 172.623 3 59.050 231.673 48.66 0.00 48.10 49.70 77.45 3428 172.623 3 59.050 231.673

咱們先看一下YGC(從應用程序啓動到採樣時發生 Young GC 的次數)和YGCT(從應用程序啓動到採樣時 Young GC 所用的時間(秒)),計算YGCT/YGC會得出,平均每次新生代的GC耗時50ms,這是一個很小的數字,經過這個結果能夠看出,咱們大可沒必要關注新生代GC對GC性能的影響。

如今來看一下FGC( 從應用程序啓動到採樣時發生 Full GC 的次數)和FGCT(從應用程序啓動到採樣時 Full GC 所用的時間(秒)),計算FGCT/FGC會得出,平均每次老年代的GC耗時19.68s。有多是執行了三次Full GC,每次耗時19.68s,也有多是有兩次只花了1s,另外一次花了58s。無論是哪種狀況,GC優化都是頗有必要的。

使用jstat命令能夠很容易地查看GC狀態,可是分析GC的最佳方式是加上-verbosegc參數來生成日誌。在以前的文章中筆者已經解釋瞭如何分析這些日誌。HPJMeter是筆者最喜歡的用於分析-verbosegc生成的日誌的工具,它簡單易用,使用HPJmeter能夠很容易地查看GC執行時間以及GC發生頻率。

此外,若是GC執行時間知足下列全部條件,就沒有必要進行GC優化了: 
+ Minor GC執行很是迅速(50ms之內) 
+ Minor GC沒有頻繁執行(大約10s執行一次) 
+ Full GC執行很是迅速(1s之內) 
+ Full GC沒有頻繁執行(大約10min執行一次)

括號中的數字並非絕對的,它們也隨着服務的狀態而變化。有些服務可能要求一次Full GC在0.9s之內,而有些則會放得更寬一些。所以,對於不一樣的服務,須要按照不一樣的標準考慮是否須要執行GC優化。

當檢查GC狀態時,不能只查看Minor GC和Full GC的時間,還必需要關注GC執行的次數。若是新生代空間過小,Minor GC將會很是頻繁地執行(有時每秒會執行一次,甚至更多)。此外,傳入老年代的對象數目會上升,從而致使Full GC的頻率升高。所以,在執行jstat命令時,請使用-gccapacity參數來查看具體佔用了多少空間。

設置GC類型/內存大小

設置GC類型

Oracle JVM有5種垃圾收集器,可是在JDK 7之前的版本中,你只能在Parallel GC, Parallel Compacting GC 和CMS GC之中選擇,至於具體選擇哪一個,則沒有具體的原則和規則。

既然這樣的話,咱們如何來選擇GC呢?最好的方法是把三種都用上,可是有一點必須明確——CMS GC一般比其餘並行(Parallel)GC都要快(這是由於CMS GC是併發的GC),若是確實如此,那隻選擇CMS GC就能夠了,不過CMS GC也不老是更快,當出現concurrent mode failure時,CMS GC就會比並行GC更慢了。

Concurrent mode failure

如今讓咱們來深刻地瞭解一下concurrent mode failure。

並行GC和CMS GC的最大區別是並行GC採用「標記-整理」(Mark-Compact)算法而CMS GC採用「標記-清除」(Mark-Sweep)算法,compact步驟就是經過移動內存來消除內存碎片,從而消除分配的內存之間的空白區域。

對於並行GC來講,不管什麼時候執行Full GC,都會進行compact工做,這消耗了太多的時間。不過在執行完Full GC後,下次內存分配將會變得更快(由於直接順序分配相鄰的內存)。

相反,CMS GC沒有compact的過程,所以CMS GC運行的速度更快。可是也是因爲沒有整理內存,在進行磁盤清理以前,內存中會有不少零碎的空白區域,這也致使沒有足夠的空間分配給大對象。例如,在老年代還有300MB可用空間,可是連一個10MB的對象都沒有辦法被順序存儲在老年代中,在這種狀況下,會報出「concurrent mode failure」的warning,而後系統執行compact操做。可是CMS GC在這種狀況下執行的compact操做耗時要比並行GC高不少,而且這還會致使另外一個問題,關於「concurrent mode failure」的詳細說明。

綜上所述,你須要根據你的系統狀況爲其選擇一個最適合的GC類型。

每一個系統都有最適合它的GC類型等着你去尋找,若是你有6臺服務器,我建議你每兩個服務器設置相同的參數,而後加上-verbosegc參數再分析結果。

設置內存大小

下面展現了內存大小、GC運行次數和GC運行時間之間的關係:

大內存空間 
+ 減小了GC的次數 
+ 提升了GC的運行時間

小內存空間 
+ 增多了GC的次數 
+ 下降了GC的運行時間

關於如何設置內存的大小,沒有一個標準答案,若是服務器資源充足而且Full GC能在1s內完成,把內存設爲10GB也是能夠的,可是大部分服務器並不處在這種狀態中,當內存設爲10GB時,Full GC會耗時10-30s,具體的時間天然與對象的大小有關。

既然如此,咱們該如何設置內存大小呢?一般我推薦設爲500MB,這不是說你要經過-Xms500m-Xmx500m參數來設置WAS內存。根據GC優化以前的狀態,若是Full GC後還剩餘300MB的空間,那麼把內存設爲1GB是一個不錯的選擇(300MB(默認程序佔用)+ 500MB(老年代最小空間)+200MB(空閒內存))。這意味着你須要爲老年代設置至少500MB空間,所以若是你有三個運行服務器,能夠把它們的內存分別設置爲1GB,1.5GB,2GB,而後檢查結果。

理論上來講,GC執行速度應該遵循1GB> 1.5GB> 2GB,1GB內存時GC執行速度最快。然而,理論上的1GB內存Full GC消耗1s、2GB內存Full GC消耗2 s在現實裏是沒法保證的,實際的運行時間還依賴於服務器的性能和對象大小。所以,最好的方法是建立儘量多的測量數據並監控它們。

在設置內存空間大小時,你還須要設置一個參數:NewRatioNewRatio的值是新生代和老年代空間大小的比例。若是XX:NewRatio=1,則新生代空間:老年代空間=1:1,若是堆內存爲1GB,則新生代:老年代=500MB:500MB。若是NewRatio等於2,則新生代:老年代=1:2,所以,NewRatio的值設置得越大,則老年代空間越大,新生代空間越小。

你可能會認爲把NewRatio設爲1會是最好的選擇,然而事實並不是如此,根據筆者的經驗,當NewRatio設爲2或3時,整個GC的狀態表現得更好。

完成GC優化最快地方法是什麼?答案是比較性能測試的結果。爲了給每臺服務器設置不一樣的參數並監控它們,最好查看的是一或兩天後的數據。當經過性能測試來進行GC優化時,你須要在不一樣的測試時保證它們有相同的負載和運行環境。然而,即便是專業的性能測試人員,想精確地控制負載也很困難,而且須要大量的時間準備。所以,更加方便容易的方式是直接設置參數來運行,而後等待運行的結果(即便這須要消耗更多的時間)。

分析GC優化的結果

在設置了GC參數和-verbosegc參數後,可使用tail命令確保日誌被正確地生成。若是參數設置得不正確或日誌未生成,那你的時間就被白白浪費了。若是日誌收集沒有問題的話,在收集一或兩天數據後再檢查結果。最簡單的方法是把日誌從服務器移到你的本地PC上,而後用HPJMeter分析數據。

在分析結果時,請關注下列幾點(這個優先級是筆者根據本身的經驗擬定的,我認爲選取GC參數時應考慮的最重要的因素是Full GC的運行時間。): 
+ 單次Full GC運行時間 
+ 單次Minor GC運行時間 
+ Full GC運行間隔 
+ Minor GC運行間隔 
+ 整個Full GC的時間 
+ 整個Minor GC的運行時間 
+ 整個GC的運行時間 
+ Full GC的執行次數 
+ Minor GC的執行次數

找到最佳的GC參數是件很是幸運的,然而在大多數時候,咱們並不會如此幸運,在進行GC優化時必定要當心謹慎,由於當你試圖一次完成全部的優化工做時,可能會出現OutOfMemoryError錯誤。

優化案例

到目前爲止,咱們一直在從理論上介紹GC優化,如今是時候將這些理論付諸實踐了,咱們將經過幾個例子來更深刻地理解GC優化。

示例1

下面這個例子是針對Service S的優化,對於最近剛開發出來的Service S,執行Full GC須要消耗過多的時間。

如今看一下執行jstat -gcutil的結果

S0 S1 E O P YGC YGCT FGC FGCT GCT 12.16 0.00 5.18 63.78 20.32 54 2.047 5 6.946 8.993

左邊的Perm區的值對於最初的GC優化並不重要,而YGC參數的值更加對於此次優化更爲重要。

平均執行一次Minor GC和Full GC消耗的時間以下表所示:

 

表3:Service S的Minor GC 和Full GC的平均執行時間

 

GC類型 GC執行次數 GC執行時間 平均值
Minor GC 54 2.047s 37ms
Full GC 5 6.946s 1.389s

37ms對於Minor GC來講還不賴,但1.389s對於Full GC來講意味着當GC發生在數據庫Timeout設置爲1s的系統中時,可能會頻繁出現超時現象。

首先,你須要檢查開始GC優化前內存的使用狀況。使用jstat -gccapacity命令能夠檢查內存用量狀況。在筆者的服務器上查看到的結果以下:

NGCMN NGCMX NGC S0C S1C EC OGCMN OGCMX OGC OC PGCMN PGCMX PGC PC YGC FGC
212992.0 212992.0 212992.0 21248.0 21248.0 170496.0 1884160.0 1884160.0 1884160.0 1884160.0 262144.0 262144.0 262144.0 262144.0 54 5

其中的關鍵值以下: 
+ 新生代內存用量:212,992 KB 
+ 老年代內存用量:1,884,160 KB

所以,除了永久代之外,被分配的內存空間加起來有2GB,而且新生代:老年代=1:9,爲了獲得比使用jstat更細緻的結果,還需加上-verbosegc參數獲取日誌,並把三臺服務器按照以下方式設置(除此之外沒有使用任何其餘參數): 
+ NewRatio=2 
+ NewRatio=3 
+ NewRatio=4

一天後我獲得了系統的GC log,幸運的是,在設置完NewRatio後系統沒有發生任何Full GC。

這是爲何呢?這是由於大部分對象在建立後很快就被回收了,全部這些對象沒有被傳入老年代,而是在新生代就被銷燬回收了。

在這樣的狀況下,就沒有必要去改變其餘的參數值了,只要選擇一個最合適的NewRatio值便可。那麼,如何肯定最佳的NewRatio值呢?爲此,咱們分析一下每種NewRatio值下Minor GC的平均響應時間。

在每種參數下Minor GC的平均響應時間以下: 
+ NewRatio=2:45ms 
+ NewRatio=3:34ms 
+ NewRatio=4:30ms

咱們能夠根據GC時間的長短得出NewRatio=4是最佳的參數值(儘管NewRatio=4時新生代空間是最小的)。在設置完GC參數後,服務器沒有發生Full GC。

爲了說明這個問題,下面是服務執行一段時間後執行jstat –gcutil的結果:

S0 S1 E O P YGC YGCT FGC FGCT GCT 8.61 0.00 30.67 24.62 22.38 2424 30.219 0 0.000 30.219

你可能會認爲是服務器接收的請求少才使得GC發生的頻率較低,實際上,雖然Full GC沒有執行過,但Minor GC被執行了2424次。

示例2

這是一個Service A的例子。咱們經過公司內部的應用性能管理系統(APM)發現JVM暫停了至關長的時間(超過8秒),所以咱們進行了GC優化。咱們努力尋找JVM暫停的緣由,後來發現是由於Full GC執行時間過長,所以咱們決定進行GC優化。

在GC優化的開始階段,咱們加上了-verbosegc參數,結果以下圖所示:

 

 

 

圖1:進行GC優化以前STW的時間

 

上圖是由HPJMeter生成的圖片之一。橫座標表示JVM執行的時間,縱座標表示每次GC的時間。CMS爲綠點,表示Full GC的結果,而Parallel Scavenge爲藍點,表示Minor GC的結果。

以前我說過CMS GC是最快的GC,可是上面的結果顯示在一些時候CMS耗時達到了15s。是什麼致使了這一結果?請記住我以前說的:CMS在執行compact(整理)操做時會顯著變慢。此外,服務的內存經過-Xms1g=Xmx4g設置了,而分配的內存只有4GB。

所以筆者將GC類型從CMS GC改成了Parallel GC,把內存大小設爲2GB,並把NewRatio設爲3。在執行jstat -gcutil幾小時後的結果以下:

S0 S1 E O P YGC YGCT FGC FGCT GCT 0.00 30.48 3.31 26.54 37.01 226 11.131 4 11.758 22.890

Full GC的時間縮短了,變成了每次3s,跟15s比有了顯著提高。可是3s依然不夠快,爲此筆者建立了如下6種狀況: 
+ Case 1: -XX:+UseParallelGC -Xms1536m -Xmx1536m -XX:NewRatio=2 
+ Case 2: -XX:+UseParallelGC -Xms1536m -Xmx1536m -XX:NewRatio=3 
+ Case 3: -XX:+UseParallelGC -Xms1g -Xmx1g -XX:NewRatio=3 
+ Case 4: -XX:+UseParallelOldGC -Xms1536m -Xmx1536m -XX:NewRatio=2 
+ Case 5: -XX:+UseParallelOldGC -Xms1536m -Xmx1536m -XX:NewRatio=3 
+ Case 6: -XX:+UseParallelOldGC -Xms1g -Xmx1g -XX:NewRatio=3

上面哪種狀況最快?結果顯示,內存空間越小,運行結果最少。下圖展現了性能最好的Case 6的結果圖,它的最慢響應時間只有1.7s,而且響應時間的平均值已經被控制到了1s之內。

 

 

 

圖2:Case 6的持續時間圖

 

基於上圖的結果,按照Case 6調整了GC參數,但這卻致使每晚都會發生OutOfMemoryError。很難解釋發生異常的具體緣由,簡單地說,應該是批處理程序致使了內存泄漏,咱們正在解決相關的問題。

若是隻對GC日誌作一些短期的分析就將相關參數部署到全部服務器上來執行GC優化,這將是很是危險的。切記,只有當你同時仔細分析服務的執行狀況和GC日誌後,才能保證GC優化沒有錯誤地執行。

在上文中,咱們經過兩個GC優化的例子來講明瞭GC優化是怎樣執行的。正如上文中提到的,例子中設置的GC參數能夠設置在相同的服務器之上,但前提是他們具備相同的CPU、操做系統、JDK版本而且運行着相同的服務。此外,不要把我使用的參數照搬到你的應用上,它們可能在你的機器上並不能起到一樣良好的效果。

總結

筆者沒有執行heap dump並分析內存的詳細內容,而是經過本身的經驗進行GC優化。精確地分析內存能夠獲得更好的優化效果,不過這種分析通常只適用於內存使用量相對固定的場景。若是服務嚴重過載並佔有了大量的內存,則建議你根據以前的經驗進行GC優化。

筆者已經在一些服務上設置了G1 GC參數並進行了性能測試,但尚未應用於正式的生產環境。G1 GC的速度快於任何其餘的GC類型,可是你必需要升級到JDK 7。此外,暫時還沒法保證它的穩定性,沒有人知道運行時是否會出現致命的錯誤,所以G1 
GC暫時還不適合投入應用。

等將來JDK 7真正穩定了(這並非說它如今不穩定),而且WAS針對JDK 7進行優化後,G1 GC最終能按照預期的那樣來工做,等到那一天咱們可能就再也不須要GC優化了。

相關文章
相關標籤/搜索