為什么需要優化GC
或者說的更確切一些,對於基於Java的服務,是否有必要優化GC?應該說,對於所有的基於Java的服務,並不總是需要進行GC優化,但前提是所運行的基於Java的系統,包含了如下參數或行為:
- 已經通過 -Xms 和–Xmx 設置了內存大小
- 包含了 -server 參數
- 系統中沒有超時日志等錯誤日志
換句話說,如果你沒有設定內存的大小,並且系統充斥着大量的超時日志時,你就需要在你的系統中進行GC優化了。
但是,你需要時刻銘記一條:GC優化永遠是最后一項任務。
你應該考慮一下進行GC的最根本原因。垃圾收集器需要清除在程序中創建的對象,GC執行的次數即需要被垃圾收集器清理的對象個數,與創建對象的數量成正比,因此,首先你應該減少創建對象的數量。
俗話說的好,“冰凍三尺非一日之寒”。我們應該從小事做起,否則日積月累就會很難管理。
- 我們需要使用StringBuilder 或者StringBuffer 來替代String
- 應該盡量少的輸出日志
但是,我們知道有些情況會讓我們束手無策,我們眼睜睜的看着XML以及JSON解析占用了大量的內存。即便我們已經盡可能少的使用String以及盡量少的輸出日志,但是大量的臨時內存仍然被用於XML或者JSON解析,例如10-100MB。但是,舍棄XML和JSON是很難的。這個適合,我們只需要知道,他會占用很多內存。
如果應用內存使用量經過幾次重復調整之后有所改善,你就可以開始GC優化了。
我覺得GC優化可以歸納了兩個目的:
- 一個是將轉移到老年代的對象數量降到最少
- 另一個是減少Full GC的執行時間
將轉移到年老代的對象數量降到最少
分代垃圾回收策略是由Oracle JVM提供,不包括可以在JDK7以及更高版本中使用的G1 GC。換句話說,對象被創建在伊甸園空間,而后轉化到幸存者空間,最終剩余的對象被送到老年代。某些比較大的對象會在被創建在伊甸園空間后,直接轉移到年老代空間。年老代空間上的GC處理會年輕代花費更多的時間。因此,減少被移到年老代對象的數據可以顯著地減少Full GC的頻率。減少被移到年老代空間的對象的數量,可能會被誤解為將對象留在新生代。但是,這是不可能的。取而代之,你可以調整新生代空間的大小。
減少Full GC執行時間
Full GC的執行時間比Minor GC要長很多。因此,如果Full GC花費了太多的時間(超過1秒),一些連接的部分可能會發生超時錯誤。
- 如果你試圖通過減少年老代空間來減少Full GC的執行時間,可能會導致OutOfMemoryError 或者 Full GC執行的次數會增加。
- 與之相反,如果你試圖通過增加老年代空間來減少Full GC執行次數,執行時間又會增加。
因此,你需要將老年代空間設定為一個“合適”的值。
影響GC性能的參數
正如我們在第二篇文章結尾提到的,不要幻想“某個人設定了GC參數后性能得到極大的提高,我們為什么不和他用一樣的參數?”,因為不同的Web服務所創建對象的大小和他們的生命周期都不盡相同。
簡單來說,如果一個任務的執行條件是A,B,C,D和E,同樣的任務執行條件換為A和B,你會覺得哪個更快?從一般人的直覺來看,在A和B條件下執行的任務會更快。
Java GC參數也是相同的道理,設定一些參數不但沒有提高GC執行速度,反而可能導致他更慢。GC優化的最基本原則是將不同的GC參數用於2台或者多台服務器,並進行對比,並將那些被證明提高了性能或者減少了GC執行時間的參數應用於服務器。請謹記這一點。
下面這個表格列出了GC參數中與內存大小相關的,可以影響性能的參數。
表1:GC優化需要考慮的Java參數
定義 | 參數 | 說明 |
堆內存 | -Xms | 啟動JVM時的堆內存空間大小 |
-Xmx | 堆內存的最大值 | |
年輕代 | -XX:NewRatio | 年輕代與年老代的比值 |
-XX:NewSize | 年輕代大小 | |
-XX:SurvivorRatio | 伊甸園空間和幸存者空間的比值 |
我在進行GC優化時經常使用-Xms,-Xmx和-XX:NewRatio。-Xms和-Xmx也是必須的。你如何設定NewRatio 會對GC性能產生十分顯著的影響。
有些人可能會問如何設定Perm區域的大小?你可以通過-XX:PermSize 和-XX:MaxPermSize參數來設定。
另一個可能影響GC性能的參數是GC類型。下表列出了所有可選的GC類型(基於JDK6.0)。
表2: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 |
在JDK6中這兩個參數必須同時使用 |
影響GC性能的參數有很多,但是上面提到的參數會帶來最顯著的效果。請牢記,設定過多的參數不一定會減少GC執行時間。除了G1 GC,可以通過每種類型第一行的參數來切換GC類型。最常用的GC類型是Serial GC。他專門針對客戶端系統進行了優化。
GC優化過程
GC優化的過程與大多數性能改善的過程及其類似。下面是我使用的GC優化過程。
2.在分析監控結果后,決定是否進行GC優化
在檢查GC狀態的過程中,你應該分析監控結果以便決定是否進行GC優化,如果分析結果表明執行GC的時間只有0.1-0.3秒,那你就沒必要浪費時間去進行GC優化。但是,如果GC的執行時間是1-3秒,或者超過10秒,GC將勢在必行。
但是,如果你已經為Java分配了10GB的內存,並且不能再減少內存大小,你將無法再對GC進行優化。在進行GC優化之前,你必須想清楚你為什么要分配如此大的內存空間。假如當你分1 GB 或 2 GB內存時出現OutOfMemoryError ,你應該執行堆內存轉儲(heap dump),並消除隱患。
注意:
堆內存轉儲是一個用來檢查Java內存中的對象和數據的文件。該文件可以通過執行JDK中的jmap命令來創建。在創建文件的過程中,Java程序會暫停,因此不要再系統執行過程中創建該文件。
你可以在互聯網上搜索堆內存[s1] 轉儲的詳細說明。對於韓國的讀者,可以參考我去年發布的書: The story of troubleshooting for Java developers and system operators (Sangmin Lee, Hanbit Media, 2011, 416 pages)。
3. 調整GC類型/內存空間
如果你已經決定要進行GC優化,那么就要選擇GC類型和設定內存空間。在這時,如果你有幾台不同服務器,請時刻牢記,檢查每一台服務器的GC參數,並進行有針對性的優化。
4.分析結果
在調整了GC參數並持續收集24小時之后,開始對結果進行分析,如果你幸運的話,你就找到那些最適合系統的GC參數。反之,你需要通過分析日志來檢查內存是如何被分配的。然后你需要通過不斷的調整GC類型和內存空間大小一邊找到最佳的參數。
5. 如果結果令人滿意,你可以將該參數應用於所有的服務器,並停止GC優化
有過GC優化結果令人滿意,你可以應用於所有的服務器,下面的章節中,我們將看到每個步驟的具體任務。
監控GC狀態及分析結果
查看運行中的Web Application Server (WAS)的GC狀態的最佳方法是通過jstat命令,在第二篇文章成為Java GC專家系列(2) ——監控Java垃圾回收中我已經詳細解釋過jstat命令,因此本篇文章我將重點描述數據部分。
下面這個例子展現了某個JVM在進行GC優化之前的狀態(很遺憾,這不是一個線上服務器)。
1
2
3
4
|
$ 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 和YGCT,計算YGCT/ YGC得到0.050秒(50毫秒)。這意味着新生代空間上的GC操作平均花費50毫秒。在這種情況,你大可不必擔心新生代空間上執行的GC操作。
接下來,我們來看一下FGCT 和FGC。,計算FGCT/ FGC得到19.68秒,這意味着GC的平均執行時間為19.68秒,可能是每次花費19.68秒執行了三次,也可能是其中的兩次執行了1秒而另一次執行了58秒。不論哪種情況,都需要進行GC優化。
通過jstat 命令可以很輕易地查看GC狀態,但是,分析GC的最佳方式是通過–verbosegc參數來生成日志,在之前的文章中我已經解釋了如何分析這些日志,HPJMeter 是我個人最喜歡的用於分析-verbosegc 日志的工具。他很易於使用和分析結果。通過HPJmeter你可以很輕易查看GC執行時間以及GC發生頻率。如果GC執行時間滿足下面所有的條件,就意味着無需進行GC優化了。
- Minor GC執行的很快(小於50ms)
- Minor GC執行的並不頻繁(大概10秒一次)
- Full GC執行的很快(小於1s)
- Full GC執行的並不頻繁(10分鍾一次)
上面提到的數字並不是絕對的;他們根據服務狀態的不同而有所區別,某些服務可能滿足於Full GC每次0.9秒的速度,但另一些可能不是。因此,針對不同的服務設定不同的值以決定是否進行GC優化。
在查看GC狀態的時候有件事你需要特別注意,那就是不要只關注Minor GC 和Full GC的執行時間。還要關注GC執行的次數,例如,當新生代空間較小時,Minor GC會過於頻繁的執行(有時每秒超過1次)。另外,轉移到老年代的對象數增多,則會導致Full GC執行次數增多。因此,別忘了加上–gccapacity參數來查看具體占用了多少空間。
設定GC類型/內存空間大小
1.設定GC類型
OracleJVM有5種GC類型,但是在JDK7之前的版本中,只能在Parallel GC, Parallel Compacting GC 和CMS GC之中選擇一個,對於選擇哪個沒有明確的原則和規則。
這樣的話,我們該如何選擇呢?強烈建議三者都選,但是,有一點是很明確的:CMS GC比Parallel GCs更快。如果真的如此,那么就選CMS GC了。但是,CMS GC也不總是更快。整體來看,CMS GC模式下的Full GC執行更快,不過,一旦出現並行模式失敗,他將比Parallel GC更慢。
CONCURRENT MODE失敗
我們來詳細講解一下concurrent mode失敗。
Parallel GC 和 CMS GC 最大的不同來自於壓縮任務。壓縮任務是通過刪除已分配內存空間中的空白空間以便壓縮內存,清理內存碎片。
在Parallel GC模式下,壓縮工作在Full GC執行時進行,這會費很多時間,但是,在執行完Full GC之后,由於能夠順序地分配空間,隨后的內存能夠被更快的分配。
與之相反的,CMS GC並不進行壓縮處理,因此,CMS GC執行的更快。但是,由於沒有壓縮,在進行磁盤清理之前,內存中會有很多空白空間。這就是說,可能沒有足夠的空間存儲大的對象,例如,雖然老年代空間還有300MB空間,但是一些10MB的對象無法被順序的存儲。在這種情況下,會出現“並行模式失敗”警告,並執行壓縮處理。在CMS GC模式下,壓縮處理的執行時間要比Parallel GCs長很多。另外,這還將導致另外一個問題。關於並發模式失敗的詳細說明,可以參考Oracle工程師撰寫的Understanding CMS GC Logs。
綜上所述,你需要找到最適合你的系統的GC類型。
每個系統都有最適合他的GC類型等着你去尋找,如果你有6台服務器。我建議你每兩台設置相同的參數。並添加 –verbosegc參數,分析結果。
2.設定內存空間大小
下面展示了內存空間大小,GC執行次數以及GC執行時間三者間的關系。
- 大內存空間
- 減小GC執行次數
- 增加GC執行時間
- 小內存空間
- 減小GC執行時間
- 增加GC執行次數
關於如何設置內存空間的大小,沒有唯一的標准答案。如果服務器資源足夠,而且Full GC也可能在1秒內完成,設置為10GB當然可行。。但絕大多數服務器並不是這樣,當內存設為10GB時,可能要花費10~30秒來執行Full GC。當然,執行時間會隨對象的大小而改變。
鑒於如此,我們應該如何設定內存空間大小呢?一般來說,我建議為500MB。不過請注意這不是讓你將WAS的內存參數設置為–Xms500m 和–Xmx500m。根據優化GC之前的狀態,如果Full GC執行之后內存空間剩余300MB,那么最好將內存設置為1GB(300MB(默認程序占用)+ 500MB(老年代最小空間)+200MB(空閑內存))。也就是說你要為老年代額外設置500MB。因此,如果你有三個執行服務器,內存分別設置為1GB,1.5GB,2GB,並且檢查結果。
理論上來講,GC執行速度應該遵循1GB> 1.5GB> 2GB,因此1GB執行GC速度最快。但是並不說明1GB空間的Full GC會花費1秒而2GB空間會花費2秒。時間取決於服務器的性能和對象的大小。因此,最佳的方式是建立盡可能多的衡量指標來監控他們。
對於內存空間大小,你應該額外設定NewRatio參數。NewRatio參數是新生代和老年代空間的比例,即XX:NewRatio=1意味着新生代與老年代之比為1:1。對於1GB來說就是新生代和老年代各500MB。如果NewRatio為2,意味着新生代老年代之比為1:2,因此該值越大,老年代空間越大,新生代空間越小。
這看似一件不是很重要的事情,但NewRatio參數會顯著地影響整個GC的性能。如果新生代空間很小,會用更多的對象被轉移到老年代空間,這樣導致頻繁的Full GC,增加暫停時間。
你可以簡單的認為NewRatio 為1是最佳的選擇,但是,有時可能設置為2或3更好,我就見過很多這樣的例子。
如何最快的完成GC優化?對比性能測試的結果應該是最快地方法,為每一台服務器設置不同的參數並監控他們的狀態,強烈建議至少監控1或2天的數據。但是,當你對GC優化是,你要確保每次執行相同的負載。並且請求的比率,例如URL都應該是一致的。不過,即便對於專業測試人員要想精確的控制負載也是很難的,並要花費大量的時間准備。因此,相對來說比較方便和容易的方法是調整才參數,之后花費較長的時間收集結果。
分析GC優化結果
在設置了GC參數以及-verbosegc參數之后,通過tail命令確保日志被正確的生成。如果參數設置的不正確或者日志沒有生成,你將白白浪費你的時間。如果日志正確的話,持續收集1到2天。隨后最好將日志下載到本地PC並用HPJMeter來分析
- Full GC 執行時間
- Minor GC執行時間
- Full GC 執行間隔
- Minor GC 執行間隔
- Entire Full GC 執行時間
- Entire Minor GC 執行時間
- Entire GC 執行時間
- Full GC e執行時間
- Minor GC 執行時間
找到最佳的GC參數是件非常幸運的事情,然而在大多數場合,我們並不會得到幸運之神的眷顧,在進行GC優化時要盡量小心謹慎,想一步完成優化往往會導致OutOfMemoryError 。
優化示例
好了,我們一直在紙上談兵,現在我們看一些實際的GC優化的例子。
示例1
下面這個例子針對 Service S的優化,對於最近被部署的 Service S,Full GC花費了太長的時間。
請看 jstat –gcutil的執行結果。
1
2
|
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.047 | 37ms |
Full GC | 5 | 6.946 | 1389ms |
對於Minor GC來說,37ms還湊合。但是對於Full GC來說,1.389s也就意味着如果你的數據庫超時時間設為1s,那在垃圾回收的時候就會出現超時操作。這種情況下,我們就需要對GC進行優化。
首先你應該知道在GC優化之前的內存使用情況。使用命令jstat –gccapacity來查看。我們的測試服務器執行命令后的結果如下:
1
2
|
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,不算Perm空間的話,新生代與老年代之比為1:9。通過jstat和-verbosegc 日志進行數據收集,並把三台服務器按照如下方式設置(不再設置其他額外參數)。
- NewRatio=2
- NewRatio=3
- NewRatio=4
一天之后,檢查系統的GC日志后發現,在設置了NewRatio參數后很幸運的沒有發生Full GC,
為什么了?因為大部分的對象在創建后很快就被垃圾回收掉(譯者注:在年輕代中),所以很多對象在沒有被轉移到年老代時就已經在年輕代被回收掉。
這種情況下,我們沒有必要再修改其他選項,只需要為NewRatio設定一個最佳值。但是,我們怎么來確定NewRatio的最佳值了?為了確定最佳值,我們需要比較幾個NewRatio下Minor GC的平均時間。
- NewRatio=2: 45 ms
- NewRatio=3: 34 ms
- NewRatio=4: 30 ms
我們看到NewRatio=4 是最佳的參數,雖然它的新生代空間最小,但GC時間也最短。設定這個參數之后,系統沒有執行過Full GC。
為了說明這個問題,下面是服務運行一段時間后執行jstat –gcutil的結果:
1
2
|
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執行頻率下降。實際上,在Minor GC執行了 2,424次的時候Full GC都沒有執行。
示例2
這是一個針對ServiceA的例子,我們通過公司內部的應用性能管理系統(APM)發現JVM暫停了相當長的時間(超過8秒),因此我們決定進行GC優化。我們找到了Full GC執行時間過長的原因,並着手解決。
進行GC優化的第一步,就是我們添加了-verbosegc參數,並得到如下結果。
圖1:進行GC優化之前的STW時間
如上圖所示,由HPJMeter自動生成的圖片之一。X坐標表示JVM執行的時間。Y坐標表示每次GC的時間。CMS綠點,表示Full GC結果。Parallel Scavenge藍點,表示Minor GC結果。
之前我曾經說過CMS GC是最快的,但是上面的的結果顯示出於某種原因,它最多花費了15秒。是什么導致這個結果?是否想起我之前提過的,CMS在進行內存清理時,會變慢。與此同時,服務的內存被設定為 –Xms1g和–Xmx4g ,且實際分配了4GB內存。
因此,我將GC類型從CMS改為Parallel GC。並且將內存改為2GB,設定NewRatio 為3。幾小時之后我使用 jstat –gcutil得到如下結果:
1
2
|
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
|
相對於4GB時的15秒,Full GC變成了平均每次3秒。但是3秒一樣比較慢,因此我設計了如下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
哪一個最快呢?結果顯示,內存越小,結果越好。下圖展示了Case6的結果。這是GC的性能最好。最長的響應時間只有1.7秒。平均時間在1秒之內。
圖2:Case6的時間圖表
基於以上結果。我們按照Case6調整了GC參數。但是,這導致了每天晚上都會發生OutOfMemoryError。在這里很難解釋具體的原因。簡單來說,批處理程序導致了內存泄漏。相關的問題已經被解決。
如果對GC日志只分析很短的時間就貿然對所有服務器進行優化是非常危險的。請時刻牢記,你必須同時分析GC日志和應用程序。
我們回顧了兩個關於GC優化的例子,正如我之前提到的,例子中提到的GC參數,可以設置在相同的服務器之上,但前提是他們具有相同的CPU,操作系統,JDK版本以及運行着相同的服務。但是不要直接把我用過的參數用到你的服務至上,它們未必能很好的工作。
結論
我憑借經驗進行GC優化,而沒有執行堆轉儲並分析內存的詳細內容。精確地分析內存可以得到更好的優化效果。但是,這種分析一般適用於內存使用量相對固定的場合。不過,如果服務嚴重過載並占用的大量的內存,強力建議根據之前的經驗進行GC優化。
我已經在一些服務上設置了G1 GC參數,並進行過性能測試。但還沒有應用與正式環境,G1 GC參數的速度要快於其他任何GC類型。但是,你必須要升級到JDK7。另外,他的穩定性也暫時沒有保障,沒人知道是否會出現致命的錯誤。因此還不到將其正式應用的時候
在未來的某一天,等到JDK7真正穩定了(這不是說他現在不穩定),並且WAS針對JDK7進行優化后,G1 GC最終能夠按照預期的那樣工作了,我們可能就不需要在進行GC優化了。