1. 程式人生 > >JavaGC(3)-Java垃圾回收優化

JavaGC(3)-Java垃圾回收優化

        本文是成為Java GC系列文章的第三篇。在第一篇中我們學習了不同GC演算法的執行過程,GC是如何工作的,什麼是新生代和老年代,你應該瞭解的JDK7中的5種GC型別,以及這5種類型對於應用效能的影響。在第二篇,解釋了JVM實際上是如何執行垃圾回收的,我們如何監控GC,以及那哪些具可以讓我們的工作更快,更高效。本篇文章中,我們會基於實際的例子來解釋一些優化GC的最佳實踐。我認為在閱讀本篇文章之前,你已經很好地理解了之前的文章,因此,為了你能夠更好地學習本文,如果你還沒有讀過之前的兩篇文章話,請先閱讀。

1、為什麼需要優化GC

       或者說的更確切一些,對於基於Java的服務,是否有必要優化GC?

應該說,對於所有的基於Java的服務,並不總是需要進行GC優化,但前提是所執行的基於Java的系統,包含了如下引數或行為:

  • 已經通過 -Xms 和–Xmx 設定了記憶體大小
  • 包含了 -server 引數
  • 系統中沒有超時日誌等錯誤日誌

       換句話說,如果你沒有設定記憶體的大小,並且系統充斥著大量的超時日誌時,你就需要在你的系統中進行GC優化了。但是,你需要時刻銘記一條:GC優化永遠是最後一項任務。

       想一下進行GC優化的最根本原因,垃圾收集器清除在Java程式中建立的物件,GC執行的次數即需要被垃圾收集器清理的物件個數,與建立物件的數量成正比,因此,首先你應該減少建立物件的數量

        俗話說的好,“冰凍三尺非一日之寒”。我們應該從小事做起,否則日積月累就會很難管理。

  • 我們需要使用StringBuilder 或者StringBuffer 來替代String
  • 應該儘量少的輸出日誌

        但是,我們知道有些情況會讓我們束手無策,我們眼睜睜的看著XML以及JSON解析佔用了大量的記憶體。即便我們已經儘可能少的使用String以及儘量少的輸出日誌,大量的臨時記憶體被用於XML或者JSON解析,例如10-100MB。但是,捨棄XML和JSON是很難的。我們只要知道,他會佔用很多記憶體。

        如果應用記憶體使用量經過幾次重複調整之後有所改善,你就可以開始GC優化了。我為GC優化歸納了兩個目的:

  1. 一個是將轉移到老年代的物件數量降到最少
  2. 另一個是減少Full GC的執行時間

        一、將轉移到老年代的物件數量降到最少

        按代的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執行次數,執行時間會增加。

        因此,你需要將老年代空間設定為一個“合適”的值。

2、影響GC效能的引數

       正如我們在第二篇文章結尾提到的,不要幻想“某個人設定了GC引數後效能得到極大的提高,我們為什麼不和他用一樣的引數?”,因為不同的Web服務所建立物件的大小和他們的生命週期都不盡相同。

       簡單來說,如果一個任務的執行條件是A,B,C,D和E,同樣的任務執行條件換為A和B,你會覺得哪個更快?從一般人的直覺來看,在A和B條件下執行的任務會更快。

Java GC引數也是相同的道理,設定一些引數不但沒有提高GC執行速度,反而可能導致他更慢。GC優化的最基本原則是將不同的GC引數用於2臺或者多臺伺服器,並進行對比,並將那些被證明提高了效能或者減少了GC執行時間的引數應用於伺服器。請謹記這一點。

下面這個表格列出了GC引數中與記憶體大小相關的,可以影響效能的引數。

表1:GC優化需要考慮的Java引數

定義

引數

描述

堆記憶體空間

-Xms

Heap area size when starting JVM

啟動JVM時的堆記憶體空間。

-Xmx

Maximum heap area size

堆記憶體最大限制

新生代空間

-XX:NewRatio

Ratio of New area and Old area

新生代和老年代的佔比

-XX:NewSize

New area size

新生代空間

-XX:SurvivorRatio

Ratio ofEdenarea and Survivor area

伊甸園空間和倖存者空間的佔比

        在進行GC優化時經常使用-Xms,-Xmx和-XX:NewRatio。-Xms和-Xmx是必須的。你如何設定NewRatio 會對GC效能產生十分顯著的影響。有些人可能會問如何設定Perm區域的大小?你可以通過-XX:PermSize 和-XX:MaxPermSize引數來設定。

        當OutOfMemoryError 錯誤發生並且是由於Perm空間不足導致時,另一個可能影響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中這兩個引數必須同時使用

        除了G1 GC,可以通過每種型別第一行的引數來切換GC型別。最常用的GC型別是Serial GC。他專門針對客戶端系統進行了優化。影響GC效能的引數有很多,但是上面提到的引數會帶來最顯著的效果。請牢記,設定過多的引數不一定會減少GC執行時間。

3、GC優化過程

      GC優化的過程與大多數效能改善的過程及其類似,下面是我們使用的GC優化過程。

       1.監控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程式會暫停,因此不要再系統執行過程中建立該檔案。你可以在網際網路上搜索堆記憶體 轉儲的詳細說明。

      3. 調整GC型別/記憶體空間

       如果你已經決定要進行GC優化,那麼就要選擇GC型別和設定記憶體空間。在這時,如果你有幾臺不同伺服器,請時刻牢記,檢查每一臺伺服器的GC引數,並進行有針對性的優化。

      4.分析結果

       在調整了GC引數並持續收集24小時之後,開始對結果進行分析,如果你幸運的話,你就找到那些最適合系統的GC引數。反之,你需要通過分析日誌來檢查記憶體是如何被分配的。然後你需要通過不斷的調整GC型別和記憶體空間大小一邊找到最佳的引數。

      5. 如果結果令人滿意,你可以將該引數應用於所有的伺服器,並停止GC優化

      如果GC優化結果令人滿意,你可以應用於所有的伺服器,下面的章節中,我們將看到每個步驟的具體任務。

4、監控GC狀態及分析結果

      檢視執行中的Web Application Server (WAS)的GC狀態的最佳方法是通過jstat命令,在第二篇文章中已經詳細解釋過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引數來檢視具體佔用了多少空間。

5、設定GC型別/記憶體空間大小

  • 設定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更慢。

併發模式失敗

        我們來詳細講解一下併發模式失敗。

        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引數,分析結果。

  • 設定記憶體空間大小

        下表展示了記憶體空間大小,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都應該是一致的。不過,即便對於專業測試人員要想精確的控制負載也是很難的,並要花費大量的時間準備。因此,相對來說比較方便和容易的方法是調整才引數,之後花費較長的時間收集結果。

6.分析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 。

7、優化示例

         好了,我們一直在紙上談兵,現在我們看一些實際的GC優化的例子。

示例1

下面這個例子針對 Service S的優化,對於最近被部署的 Service S,Full GC花費了太長的時間。

請看 jstat –gcutil的執行結果。

1 2 S0 S1 E O P YGC YGCT FGC FGCT GCT 12.160.00 5.1863.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

37 ms

Full GC

5

6.946

1,389 s

最重要的是下面兩個資料

  • 新生代實際使用空間: 212,992 KB
  • 老年代實際使用空間: 1,884,160 KB

因此,總的記憶體空間為2GB,不算Perm空間的話,新生代與老年代之比為1:9。通過jstat和-verbosegc 日誌進行資料收集,並把三臺伺服器按照如下方式設定。

  • NewRatio=2
  • NewRatio=3
  • NewRatio=4

一天之後,檢查系統的GC日誌後發現,在設定了NewRatio引數後很幸運的沒有發生Full 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.610.00 30.6724.62 22.38 2424 30.219 0 0.000 30.219

你可能會認為因為伺服器接受的請求少才導致的GC執行頻率下降。實際上,雖然Full GC沒有執行,但是Minor GC被執行了 2,424次。

示例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.0030.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版本以及執行著相同的服務。但是不要直接把我用過的引數用到你的服務至上,它們未必能很好的工作。

8、結論

        憑藉經驗進行GC優化,而沒有執行堆轉儲並分析記憶體的詳細內容。精確地分析記憶體可以得到更好的優化效果。但是,這種分析一般適用於記憶體使用量相對固定的場合。不過,如果服務嚴重過載並佔用的大量的記憶體,強力建議根據之前的經驗進行GC優化。我們已經在一些服務上設定了G1 GC引數,並進行過效能測試。但還沒有應用與正式環境,G1 GC引數的速度要快於其他任何GC型別。但是,你必須要升級到JDK7。另外,他的穩定性也暫時沒有保障,沒人知道是否會出現致命的錯誤。因此還不到將其正式應用的時候。在未來的某一天,等到JDK7真正穩定了(這不是說他現在不穩定),並且WAS針對JDK7進行優化後,G1 GC最終能夠按照預期的那樣工作了,我們可能就不需要在進行GC優化了。