英文原文:cubrid,編譯:ImportNew- 王曉傑 html
本文是成爲Java GC專家系列文章的第三篇。在第一篇《成爲JavaGC專家Part I — 深刻淺出Java垃圾回收機制》中咱們學習了不一樣GC算法的執行過程,GC是如何工做的,什麼是新生代和老年代,你應該瞭解的JDK7中的5種GC類型,以及這5種類型對於應用性能的影響。 java
在第二篇《成爲JavaGC專家Part II — 如何監控Java垃圾回收機制》,我解釋了JVM其實是如何執行垃圾回收的,咱們如何監控GC,以及那哪些具可讓咱們的工做更快,更高效。在第三篇文章中,咱們會基於實際的例子來解釋一些優化GC的最佳實踐。我認爲在閱讀本篇文章以前,你已經很好地理解了以前的文章,所以,爲了你可以更好地學習本文,若是你尚未讀過以前的兩篇文章話,請先閱讀。 算法
爲何須要優化GC 服務器
或者說的更確切一些,對於基於Java的服務,是否有必要優化GC?應該說,對於全部的基於Java的服務,並不老是須要進行GC優化,但前提是所運行的基於Java的系統,包含了以下參數或行爲: 併發
換句話說,若是你沒有設定內存的大小,而且系統充斥着大量的超時日誌時,你就須要在你的系統中進行GC優化了。 oracle
可是,你須要時刻銘記一條:GC優化永遠是最後一項任務。 jvm
想一下進行GC優化的最根本緣由,垃圾收集器清除在Java程序中建立的對象,GC執行的次數即須要被垃圾收集器清理的對象個數,與建立對象的數量成正比,所以,首先你應該減小建立對象的數量。 ide
俗話說的好,「冰凍三尺非一日之寒」。咱們應該從小事作起,不然日積月累就會很難管理。 工具
可是,咱們知道有些狀況會讓咱們一籌莫展,咱們眼睜睜的看着XML以及JSON解析佔用了大量的內存。即使咱們已經儘量少的使用String以及儘可能少的輸出日誌,大量的臨時內存被用於XML或者JSON解析,例如10-100MB。可是,捨棄XML和JSON是很難的。咱們只要知道,他會佔用不少內存。 性能
若是應用內存使用量重複幾回調整以後增長了,你就能夠開始GC優化了。
我爲GC優化概括了兩個目的:
將轉移到老年代的對象數量降到最少
按代的GC機制由Oracle JVM提供,不包括能夠在JDK7以及更高版本中使用的G1 GC。換句話說,對象被建立在伊甸園空間,然後轉化到倖存者空間,最終剩餘的對象被送到老年代。某些比較大的對象會在被建立在伊甸園空間後,直接轉移到老年代空間。老年代空間上的GC處理會新生代花費更多的時間。所以,減小被移到老年代對象的數據能夠顯著地減小Full GC的頻率。減小被移到老年代空間的對象的數量,可能被誤解爲將對象留在新生代。可是,這是不可能的。取而代之,你能夠調整新生代空間的大小。
減小Full GC執行時間
Full GC的執行時間比Minor GC要長不少。所以,若是Full GC花費了太多的時間(超過1秒),一些鏈接的部分可能會發生超時錯誤。
所以,你須要將老年代空間設定爲一個「合適」的值。
影響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 |
|
Parallel Compacting GC |
-XX:+UseParallelOldGC |
|
CMS GC |
-XX:+UseConcMarkSweepGC |
|
G1 |
-XX:+UnlockExperimentalVMOptions |
在JDK6中這兩個參數必須同時使用 |
除了G1 GC,能夠經過每種類型第一行的參數來切換GC類型。最經常使用的GC類型是Serial GC。他專門針對客戶端系統進行了優化。
影響GC性能的參數有不少,可是上面提到的參數會帶來最顯著的效果。請牢記,設定過多的參數不必定會減小GC執行時間。
GC優化過程
GC優化的過程與大多數性能改善的過程及其相似。下面是我使用的GC優化過程。
1.監控GC狀態
首先你須要監控GC來檢查在系統執行過程當中GC的各類狀態。請參考前一篇文章中提到的監控方法 成爲JavaGC專家Part II — 如何監控Java垃圾回收機制。
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命令,在第二篇文章成爲JavaGC專家Part II — 如何監控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優化了。
上面提到的數字並非絕對的;他們根據服務狀態的不一樣而有所區別,某些服務可能知足於Full GC每次0.9秒的速度,但另外一些可能不是。所以,針對不一樣的服務設定不一樣的值以決定是否進行GC優化。
在查看GC狀態的時候有件事你須要特別注意,那就是不要只關注Minor GC 和Full GC的執行時間。還要關注GC執行的次數,例如,當新生代空間較小時,Minor GC會過於頻繁的執行(有時每秒超過1次)。另外,轉移到老年代的對象數增多,則會致使Full GC執行次數增多。所以,別忘了加上–gccapacity參數來查看具體佔用了多少空間。
設定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執行時間三者間的關係。
關於如何設置內存空間的大小,沒有惟一的標準答案。若是服務器資源足夠,並且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來分析
找到最佳的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.160.005.1863.7820.32542.04756.9468.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 |
最重要的是下面兩個數據
所以,總的內存空間爲2GB,不算Perm空間的話,新生代與老年代之比爲1:9。經過jstat和-verbosegc 日誌進行數據收集,並把三臺服務器按照以下方式設置。
一天以後,檢查系統的GC日誌後發現,在設置了NewRatio參數後很幸運的沒有發生Full GC,
爲何?
咱們看到NewRatio=4 是最佳的參數,雖然它的新生代空間最小,但GC時間確最短。設定這個參數以後,系統沒有執行過Full GC。
爲了說明這個問題,下面是服務之星一段時間後執行jstat –gcutil的結果
1
2
|
S0 S1 E O P YGC YGCT FGC FGCT GCT
8.610.0030.6724.6222.38242430.21900.00030.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.483.3126.5437.0122611.131411.75822.890
|
相對於4GB時的15秒,Full GC變成了平均每次3秒。可是3秒同樣比較慢,所以我設計了以下6種場景。
那一個最快呢?結果顯示,內存越小,結果越好。下圖展現了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優化了。
想了解GC優化的更多內容,請登陸Slideshare.com 查看關聯資源。強烈推薦Everything I Ever Learned About JVM Performance Tuning @Twitter 。做者Attila Szegedi,一位Twitter工程師。請花些時間閱讀。
By Sangmin Lee, NHN Performance Engineering Lab.
做者Sangmin Lee,就任於NHN性能工程研究院