[譯]GC專家系列3-GC調優

原文連接:http://www.cubrid.org/blog/dev-platform/how-to-tune-java-garbage-collection/java

本篇是GC專家系列的第三篇。在第一篇理解Java垃圾回收中咱們學習了幾種不一樣的GC算法的處理過程,GC的工做方式,新生代與老年代的區別。因此,你應該已經瞭解了JDK 7中的5種GC類型,以及每種GC對性能的影響。算法

在第二篇Java垃圾回收的監控中介紹了在真實場景中JVM是如何運行GC,如何監控GC數據以及有哪些工具可用來方便進行GC監控。segmentfault

在本篇中,我將基於真實的案例來介紹一些GC調優的最佳選項。寫本篇文章時,我假設你已經理解了前兩篇的內容。爲了深刻理解本部份內容,你最好先瀏覽一下前兩篇的內容——若是你還沒有了解的話。服務器

GC調優是必須的嗎

更精確的說,基於Java的服務是否必定須要GC調優?應該說,GC調優並不是全部Java服務都必須作的事情。固然這是基於你已經使用了下面的選項或事實:併發

  • 經過-Xms-Xmx選項指定了內存大小oracle

  • 使用了-server選項app

  • 系統未產生太多超時日誌jvm

也就是說,若是你未設置內存大小而且你的系統產生了過多的超時日誌,恭喜你須要爲你的系統執行GC調優ide

可是,請記住:GC調優是不得已時的選擇工具

思考一下GC調優的深層緣由。垃圾回收器會去清理Java中建立的對象。GC須要清理的對象數據以及GC執行的次數取決於應用建立對象的多少。所以,爲了控制GC的執行,首先你須要減小對象的建立

俗話說「積重難返」。因此咱們須要從小處着手,不然它們將不斷壯大直到難以管理。

  • 應該多使用StringBuilderStringBuffer對象替代String

  • 減小沒必要要的日誌輸出。

即使如此,面對有些場景咱們依然無能爲力。咱們知道解析XML和JSON會佔用大量的內存空間。即使咱們儘量少的使用String,儘量好的優化日誌輸出,然而在解析XML和JSON時仍然會有大量的內存開銷,甚至有10~100MB之多,可咱們很難杜絕XML和JSON的使用。可是請記住:XML和JSON會帶來很大的內存開銷。

若是應用的內存佔用不斷提高,你就要開始對其進行GC調優了。我把GC調優的目標分爲如下兩類:

  • 下降移動到老年代的對象數量

  • 縮短Full GC的執行時間

下降移動到老年代的對象數量

在Oracle JVM中除了JDK 7及最高版本中引入的G1 GC外,其餘的GC都是基於分代回收的。也就是對象會在Eden區中建立,而後不斷在Survivor中來回移動。以後若是該對象依然存活,就會被移到老年代中。有些對象,由於佔用空間太大以至於在Eden區中建立後就直接移動到了老年代。老年代的GC較新生代會耗時更長,所以減小移動到老年代的對象數量能夠下降full GC的頻率。減小對象轉移到老年代可能會被誤解爲把對象保留在新生代,然而這是不可能的,相反你能夠調整新生代的空間大小

縮短Full GC耗時

Full GC的單次執行與Minor GC相比,耗時有較明顯的增長。若是執行Full GC佔用太長時間(例如超過1秒),在對外服務的鏈接中就可能會出現超時。

  • 若是企圖經過縮小老年代空間的方式來下降Full GC執行時間,可能會面臨OutOfMemoryError或者帶來更頻繁的Full GC。

  • 若是經過增長老年代空間來減小Full GC執行次數,單次Full GC耗時將會增長。

所以,須要爲老年代空間設置適當的大小

影響GC性能的選項

理解Java垃圾回收的結尾,我說過不要有這樣的想法:別人經過某個GC選項得到了明顯的性能提高,爲何我不直接用這個選項呢。由於不一樣的服務所擁有的對象數量和對象的生命週期是不一樣的

一個簡單場景,若是執行一個任務須要五個條件:A, B, C, D和E,另一個任務只須要兩個條件A和B,哪一個任務會快一些?一般只須要條件A和B的任務會快一些。

Java GC選項的設置也是同樣的道理。設置不少選項未必能提升GC執行速度,相反還可能會更加耗時。GC調優的基本規則是對兩臺或更多的服務器設置不一樣的選項,並對比性能表現,而後把被證實能提高性能的選項添加到應用服務器上。請記住這一點。

下表列出了與內存相關的且會影響性能的GC選項:

表1: GC調優須要關注的選項

分類 選項 說明
堆空間 -Xms 啓動JVM時的初始堆空間大小
-Xmx 堆空間最大值
新生代空間 -XX:NewRatio 新生代與老年代的比例
-XX:NewSize 新生代大小
-XX:SurvivorRatio Eden區與Survivor區的比例

我常常會使用的選項是:-Xms, -Xmx-XX:NewRatio,其中-Xms-Xmx是必須的。而如何設置-XX:NewRatio對性能會有顯著的影響。

可能有人會問如何設置永久代(Perm)的大小, 可使用-XX:PermSize-XX:MaxPermSize進行設置,但記住只有發生由Perm空間不足致使的OutOfMemoryError時才須要設置。

另一個會影響GC性能的選項是GC類型,下表列出了JDK 6.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中使用G1時,這兩個選項必須同時設置

除了G1,其餘GC類型都是經過每一個選行列的第一行選項進行設置。一般最不會使用的是Serial GC,它是爲client應用優化和設計的。

還有不少其餘影響GC性能的選項,但不如上面這些對性能的影響明顯。另外設置更多選項未必能優化GC的執行時間。

GC調優過程

GC調優過程與通常的性能改進流程很類似,下面會介紹我在GC調優過程當中的流程。

1. 監控GC狀態

首先須要監控GC狀態信息以明確在GC操做過程當中對系統的影響。具體方式能夠回顧上一篇文章:Java 垃圾回收的監控

2. 分析監控數據並決定是否須要GC調優

而後經過GC操做狀態,對監控結果進行分析,並判斷是否有必要進行GC調優。若是分析結果顯示GC耗時在0.1-0.3秒之內的話,通常不須要花費額外的時間作GC調優。然而,若是GC耗時達到1-3秒甚至10秒以上,就須要當即對系統進行GC調優

可是若是你的應用分配了10GB的內存,且不能下降內存容量的話,實際上是沒辦法進行GC調優的。這種狀況下,你首先要去思考爲何須要分配這麼大的內存。若是隻給應用分配了1GB或者2GB內存,當有OutOfMemeoryError發生時,你須要經過堆dump來分析驗證內存溢出的緣由並進行修復。

註釋:
堆dump是把內存狀況按必定格式輸出到文件,可用於檢查Java 內存中的對象和數據狀況。可以使用JDK中內置的jmap命令建立堆dump文件。建立文件過程當中,Java進程會中斷,所以不要在正常運行時系統上作此操做。

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

若是決定作GC調優,就須要考慮如何選擇GC類型、如何設置內存大小。若是你有多臺服務器,可經過爲每臺服務器設置不一樣的GC選項並對比不一樣的表現,這一步很重要。

4. 分析GC調優結果

設置GC選項後,至少要收集24小時的GC表現數據,而後就能夠着手分析這些數據了。若是足夠幸運,經過分析就恰好找到了最合適的GC選項。不然就須要分析GC日誌,並分析內存的分配狀況。而後經過不一樣的調整GC類型和內存大小來找到系統的最優選項。

5. 若是結果可接受,則對全部服務應用調優選項並中止調優

若是GC結果使人滿意,就能夠把相應的選項應用到全部服務器並中止GC調優。

下面的章節會詳細介紹每一個步驟中的詳細過程。

監控GC狀態並分析GC結果

監控Web應用(WAS: Web Application Server)GC運行狀態的最好方式是使用jstat命令。在Java 垃圾回收的監控部分已經介紹瞭如何使用jstat命令,因此這裏就直接介紹怎麼樣來校驗結果數據。

下面的例子中列出了JVM未作GC調優時的數據:

$ 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算出平均單次YGC耗時爲0.05秒。也就是說在新生代執行一次垃圾回收的平均耗時爲50毫秒。經過這份結果,咱們能夠無須關注新生代的垃圾回收。

而後再看一下FGCT和FGC,FGCT除以FGC算出平均單次FGC耗時爲19.68秒。也就是平均須要消耗19.68秒來執行一次Full GC。上面的結果(共3次Full GC)多是每次Full GC都耗時19.68秒,也有多是其中兩次都只耗時1秒,而另一次卻消耗了58秒。然而無論哪一種狀況,都迫切須要進行GC調優。

固然也能夠經過jstat來校驗結果,不過度析GC的最好方式是使用-verbosegc選項來啓動JVM。在前面的文章中我已經詳細介紹了生成日誌的方式以及如何進行分析。就分析-verbosegc日誌而言,HPJMeter是我最偏心的工具,由於它簡單易用。使用HPJMeter能夠輕鬆獲取GC執行時間的開銷以及GC發生的頻率。

若是GC執行時間知足如下判斷條件,那麼GC調優並沒那麼必須。

  • Minor GC執行迅速(50毫秒之內)

  • Minor GC執行不頻繁(間隔10秒左右一次)

  • Full GC執行迅速(1秒之內)

  • Full GC執行不頻繁(間隔10分鐘左右一次)

括號內的值並不是絕對,依據應用的服務狀態會有不一樣。有些服務可能要求Full GC處理速度不能超過0.9秒,另一些服務可能會寬鬆些。所以校驗GC結果並根據具體的服務須要,決定是否要進行GC調優。

在校驗GC狀態時,不要只關心Minor GC和Full GC的耗時,也要GC執行次數也一樣重要。若是新生代過小,Minor GC就會頻繁執行(甚至每間隔1秒就要執行一次)。另外,新生代過小致使轉移到老年代的對象增多,也會引發Full GC的頻繁執行。所以使用`-gccapacity`配合jstat命令,以檢查內存空間的使用狀況。

設置GC類型和內存大小

設置GC類型

Oracle JVM提供了5種GC類型,若是是低於JDK 7的版本,可使用Parallel GC, Parallel Compacting GC, CMS GC。固然,到底選哪個並無統一的準則或標準。

因此如何選擇合適的GC類型?推薦方案是將這三種GC都應用到應用中進行對比。不過能夠明確的是CMS GC確定比Parallel GCs更快,即然這樣只使用CMS GC便好。然而CMS GC也有出問題的時候,一般Full GC中使用CMS GC會執行更快,若是CMS GC的併發模式失敗,則會出現比Parallel GCs慢的狀況。

併發模式失敗

咱們來深刻看一下併發模式失敗的場景。

Parallel GC與CMS GC最大的區別在於壓縮任務。壓縮任務經過壓縮內存使用來移除內存中的碎片空間,以清理兩塊已分配使用的內存空間中的間隙。

在Parallel GC中,只要執行Full GC便會進行內存壓縮,所以耗時更長。不過Full GC以後,由於壓縮的原故,能夠分配連續的空間,因此內存的分配速度爲更快一些。

與之相反,CMS GC的執行中並不會伴隨內存壓縮,所以GC速度會更快一些。然而,由於未作內存壓縮, GC清理過程當中釋放的內存便會成爲空閒空間。由於空間不連續,可能會致使在建立大對象時空間不足。例如,若是老年代尚有300M空閒,卻不能爲10MB的對象分配足夠的連續空間。這時便會發生併發模式失敗的警告,並觸發內存壓縮。若是使用CMS GC,在內存壓縮過程當中可能會比Parallel GCs更爲耗時,也可能會帶來其餘問題。關於"併發模式失敗"更詳細的介紹能夠看Oracle 工程師的文章:理解CMS GC 日誌

結論就是,要爲你的系統尋找合適的GC類型。

每一個系統都有一個最適當的GC類型,因此你須要找到這個GC類型。若是你有6臺服務器,建議你爲每兩組設置相同的選項,並經過-verbosegc選項對結果進行分析和比較。

調整內存大小

下面先列出內存大小與GC執行次數、每次GC耗時之間的關係:

  • 大內存

    • 會下降GC執行次數

    • 相應的會增長GC執行耗時

  • 小內存

    • 會縮知單次GC耗時

    • 相應的會增長GC執行次數

固然,關於使用大內存仍是小內存並無惟一正確的答案。若是服務器資源足夠且Full GC執行耗時能控制在1秒之內,使用10GB的內存也是能夠的。但大多數時候若是設置內存爲10GB,GC執行效果並不盡人意,執行一次Full GC可能要消耗10~30秒(具體時長也會根據對象大小狀況而不一樣)。

既然如此,如何正確設置內存大小。一般狀況下,我會推薦500MB大小。這不是說你要把本身的WAS(Web Application Server)內存選項設置爲-Xms500-Xmx500m。基於當前未調優時的場景,檢查Full GC以後內存大小變化。若是Full GC以後尚有300MB空間剩餘,這樣最好把內存設置到1GB(300MB(默認使用) + 500MB(老年代最小容量) + 200MB(空閒空間))。這意味着你應該才老年代至少設置500MB空間。若是你有3臺服務器,能夠分別設置1GB、1.5GB和2GB,並檢查每臺機器的執行結果。

理論上,根據內存大小不一樣單次執行GC速度應該是1GB > 1.5GB > 2GB,因此1GB的內存會中三個之中GC速度最快的。但並不能保證1GB的內存Full GC耗時1秒,2GB的內存Full GC耗時2秒。實際耗時與機器性能和對象大小也有關係。因此最好的度量方式是設置每種可能性並分析他們的監控結果。

有設置內存大小時,還須要設置另一選項:NewRatioNewRatio是新生代與老年代的比值的倒數(即老年代與新生代的比值)。若是XX:NewRatio=1,就是說新生代 : 老年代的比值爲1:1。對於1GB內存,就是新生代與老年代各500MB。若是NewRatio的值是2,則是新生代 : 老年代的值爲1:2。所以比值設置的越大,老年代的空間就越大,相應的新生代空間會越小。

設置NewRatio也不是一件重要的事,但可能會對整個GC性能帶來嚴重影響。若是新生代過小,對象就會轉移到老年代,引發頻繁的Full GC,致使更多的耗時。

你可能簡單的認爲設置NewRatio=1會帶來最佳的效果,然而並不是如此。把NewRatio設置爲2或3更容易帶來好的GC表現。固然我也實際遇到過一些這樣的例子。

完成GC調優的最快途徑是什麼?經過對比性能測試的結果是獲得GC調優結果的最快途徑。經過爲每一個服務器設置不一樣的選項並觀察GC狀態,最好能觀察1到2天的數據。若是是經過性能測試來作GC調優的話,要爲每一個服務器準備相同的負載和業務操做。請求比例的分配也要與業務條件相一致。然而即使是專業的性能測試人員,準備精確的負載數據也並不是易事,一般須要花費很大精力來作準備。因此更簡捷的GC調優方式就是對業務應用準備GC選項,而後經過等待GC結果並進行分析,儘管可能須要更長的等待時間。

分析GC調優結果

在應用GC選項並設置-verbosegc後,能夠經過tail命令檢查日誌是否定期望的方式正常輸出。若是選項未精確的設置或者沒有定期望輸出,你所花費的時間都將白費。若是日誌輸出與指望相符,等待1到2天的運行後即可檢查和分析結果。最簡單的方式是把日誌文件複製到本地PC,並使用HPJMeter進行分析。

分析過程當中主要關注如下數據,下面列表是按我本身定義的優先級列出的。其中決定GC選項的最重要的數據是Full GC執行時間。

  • Full GC(平均)耗時

  • Minor GC(平均)耗時

  • Full GC執行間隔

  • MinorGC執行間隔

  • Full GC總體耗時

  • Minor GC總體耗時

  • GC總體耗時

  • Full GC執行次數

  • Minor GC執行次數

若是足夠幸運,你能剛好找到合適的GC選項,一般你並沒這麼幸運。執行GC調優時必定要格外當心,由於若是你試圖一次就完成GC調優,獲得的可能會是OutOfMemoryError

調優案例

上面咱們對於GC調優的討論還僅是紙上談兵,如今開始咱們看一些具體的GC調優的案例。

案例1

這個例子是爲服務S進行的GC優化。對於這個新上線的服務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

在開始進行調優時不用太關心持久代空間的設置,相對而言YGC的數值更值得關注。

從上面的結果中咱們可算出執行Minor GC和Full GC的平均時間上的開銷,以下表:

表3:服務S執行Minor GC和Full GC的平均耗時

GC類型 GC 執行次數 GC執行時間 平均耗時
Minor GC 54 2.047 37 ms
Full GC 5 6.946 1389 ms

對於Minor GC來講,37 ms還不算壞,而Full GC的平均耗時1.389 s對於系統來講在執行Full GC時可能會致使頻繁的超時現象,例如DB超時設置爲1 s的話就會發生超時。因此這個案例中的系統須要進行GC調優。

首先在開始GC調優以前先檢查當前的內存設置。可使用jstat -gccapacity選項查看內存的使用狀況。下面是服務S的檢查結果:

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(約208 MB)

  • 老年代使用:1,884,160 KB(約1.8 GB)

因此除去持久代以外的內存分配爲2 GB,且新生代 : 老年代爲 1:9 (即NewRatio=9)。爲了看到更詳細的信息,對系統的三個不一樣實現均設置了-verbosegc並分別設置了NewRatio選項,除此以外未添加其餘選項。

  • NewRatio = 2

  • NewRatio = 3

  • NewRatio = 4

一天以後檢查GC時日誌時幸運的發生,在設置NewRatio以後還沒有有Full GC發生。

發生了什麼?由於大多數對象在建立以後不久就被銷燬,因此新生代裏的對象在移到老年代以前就被銷燬掉了。

既然如此,就不必再設置其餘選項,只是選擇好最佳的NewRatio便可。如何選取最佳NewRatio?只能逐個分析設置不一樣NewRatio值時的Minor GC的平均耗時。

上面三個NewRatio設置對應的Minor GC平均耗時以下:

  • NewRatio=2: 45ms

  • NewRatio=3: 34ms

  • NewRatio=4: 30ms

由於NewRatio=4時Minor 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發生頻率較低,然而在Minor GC執行了2,424次的狀況下系統未發生Full GC。

案例2

下面介紹的是服務A的例子。咱們在公司的應用性能管理平臺(APM: Application Performance Manager)上發現服務A的JVM週期性的出現長時間的停頓(超過8秒未有響應)的現象。因此咱們決定對其進行GC調優。通過排查咱們發現此係統在執行Full GC時太過耗時,須要進行優化。

在着手優化以前,咱們爲系統加上了-verbosegc選項,輸出結果以下圖:

GC調優以前的GC耗時
圖1:GC調優以前的GC耗時

上圖是HPJMeter自動分析結果後提供的系統GC隨着JVM運行的耗時圖。X-軸是JVM從啓動後的運行時間軸,Y-軸是每次GC的響應時間。其中綠色的是Full GC使用的CMS垃圾回收的耗時,藍色的是Minor GC使用的Parallel Scavenge垃圾回收的耗時。

前面我說過CMS GC是最快的,但上圖可看到有場景耗時竟達到15秒之多。什麼緣由致使這種後果?回想一下我前面說過的:當內存壓縮時CMS將會變慢。另外服務A設置了-Xms1g-Xmx4g的選項,操做系統爲其分配的內存爲4 GB。

而後我把GC類型由GMS換成了Parallel GC,並把內存大小設置爲2G,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的速度提高了,與4GB內存時的15秒相比,如今平均每次只須要3秒。但3秒仍然不盡人意,因此我設計瞭如下六組選項:

  • -XX:+UseParallelGC -Xms1536m -Xmx1536m -XX:NewRatio=2

  • -XX:+UseParallelGC -Xms1536m -Xmx1536m -XX:NewRatio=3

  • -XX:+UseParallelGC -Xms1g -Xmx1g -XX:NewRatio=3

  • -XX:+UseParallelOldGC -Xms1536m -Xmx1536m -XX:NewRatio=2

  • -XX:+UseParallelOldGC -Xms1536m -Xmx1536m -XX:NewRatio=3

  • -XX:+UseParallelOldGC -Xms1g -Xmx1g -XX:NewRatio=3

哪個會更快呢?結果顯示內存越小,速度越快。下圖是第六組選項的GC持續時長分佈圖,表明了最優的GC性能提高。圖中看到最慢的爲1.7秒,而平均值下降到1秒之內。

使用第六組選項後的GC耗時
圖2:使用第六組選項後的GC耗時

所以我把服務A的GC選項調整爲了第六組中的設置,然而天天夜裏卻連續發生了OutOfMemoryError。箇中艱辛再也不細說,簡而言之就是批量的數據處理任務致使了JVM內存泄露。到此爲止,全部的問題都明瞭了。

若是隻對GC日誌作短期的觀察例把GC調優的結果應用到全部服務器上是一件很是危險的事情。必定要記住,若是GC調優可以順利執行而無端障只有一條途徑:像分析GC日誌同樣分析系統的每個服務操做。

上面經過兩個GC調優的案例演示了GC調優的具體處理過程。如我所述,案例中的GC選項能夠不作調整的應用到那些具備相同CPU、操做系統和 JDK 版本以及執行相同功能的服務上去。然而不要把這些選項應用到你的系統上,由於他們未必適用。

總結

我執行GC調優通常基於經驗而無需經過堆dump後對內存進行詳細的分析,儘管精確的內存狀態可能會帶來更好的GC調優結果。在通常情景,若是內存負載較低時,經過分析內存對象可能效果更好,不過若是服務負載較高,內存空間使用較多時,更推薦基於經驗來作GC調優。

我曾經在一些服務上對G1 GC作過性能測試,不過尚未全面使用。結果證實G1 GC執行速度比其餘任何GC都要快,不過須要把JDK升級到 JDK 7 才能享受到G1帶來的性能提高,另外G1的穩定性目前尚不能徹底保證,沒有人知道是否會帶來嚴重的bug。因此大範圍使用 G1 還尚待時日。

當 JDK 7 穩定之後(並非說它當前不穩定),而且WAS針對JDK 7作過優化以後,G1也許會穩定的運行在服務器上,到那時也許就再也不須要進行GC調優了。

更多GC調優的細節能夠在Slideshare上搜索相關材料。我最推薦的是Twitter 工程師 Attila Szegedi寫的這篇我在Twitter學到的關於JVM調優的一切,有時間能夠學習一下。

做者:Sangmin Lee, 性能實驗室高級工程師,NHN公司

相關文章
相關標籤/搜索