如何合理的規劃一次jvm效能調優
原文中的評論很有參考價值,轉發只是為了以後方便檢視
這是jvm優化系列第三篇:
JVM效能調優涉及到方方面面的取捨,往往是牽一髮而動全身,需要全盤考慮各方面的影響。但也有一些基礎的理論和原則,理解這些理論並遵循這些原則會讓你的效能調優任務將會更加輕鬆。為了更好的理解本篇所介紹的內容。你需要已經瞭解和遵循以下內容:
1、已瞭解jvm 垃圾收集器
2、已瞭解jvm 效能監控常用工具
3、能夠讀懂gc日誌
4、確信不為了調優而調優,jvm調優不能解決一切效能問題
這些內容在之前的兩篇文章已經介紹過了,如果有不瞭解的可以去點選上述連線進行回顧,如果對這些不瞭解不建議讀本篇文章。
本篇文章基於jvm效能調優,結合jvm的各項引數對應用程式調優,主要內容有以下幾個方面:
1、jvm調優的一般流程
2、jvm調優所要關注的幾個效能指標
3、jvm調優需要掌握的一些原則
4、調優策略&示例
一、效能調優的層次
為了提升系統性能,我們需要對系統的各個角度和層次來進行優化,以下是需要優化的幾個層次。
從上面我們可以看到,除了jvm調優以外,還有其他幾個層面需要來處理,所以針對系統的調優不是隻有jvm調優一項,而是需要針對系統來整體調優,才能提升系統的效能。本篇只針對jvm調優來講解,其他幾個方面,後續再介紹。
在進行jvm調優之前,我們假設專案的架構調優和程式碼調優已經進行過或者是針對當前專案是最優的。這兩個是jvm調優的基礎,並且架構調優是對系統影響最大的 ,我們不能指望一個系統架構有缺陷或者程式碼層次優化沒有窮盡的應用,通過jvm調優令其達到一個質的飛躍,這是不可能的。
另外,在調優之前,必須得有明確的效能優化目標, 然後找到其效能瓶頸。之後針對瓶頸的優化,還需要對應用進行壓力和基準測試,通過各種監控和統計工具,確認調優後的應用是否已經達到相關目標。
二、jvm調優流程
調優的最終目的都是為了令應用程式使用最小的硬體消耗來承載更大的吞吐。jvm的調優也不例外,jvm調優主要是針對垃圾收集器的收集效能優化,令執行在虛擬機器上的應用能夠使用更少的記憶體以及延遲獲取更大的吞吐量。當然這裡的最少是最優的選擇,而不是越少越好。
1、效能定義
要查詢和評估器效能瓶頸,首先要知道效能定義,對於jvm調優來說,我們需要知道以下三個定義屬性,依作為評估基礎:
- 吞吐量:重要指標之一,是指不考慮垃圾收集引起的停頓時間或記憶體消耗,垃圾收集器能支撐應用達到的最高效能指標。
- 延遲:其度量標準是縮短由於垃圾啊收集引起的停頓時間或者完全消除因垃圾收集所引起的停頓,避免應用執行時發生抖動。
- 記憶體佔用:垃圾收集器流暢執行所需要 的記憶體數量。
這三個屬性中,其中一個任何一個屬性效能的提高,幾乎都是以另外一個或者兩個屬性效能的損失作代價,不可兼得,具體某一個屬性或者兩個屬性的效能對應用來說比較重要,要基於應用的業務需求來確定。
2、效能調優原則
在調優過程中,我們應該謹記以下3個原則,以便幫助我們更輕鬆的完成垃圾收集的調優,從而達到應用程式的效能要求。
1. MinorGC回收原則: 每次minor GC 都要儘可能多的收集垃圾物件。以減少應用程式發生Full GC的頻率。
2. GC記憶體最大化原則:處理吞吐量和延遲問題時候,垃圾處理器能使用的記憶體越大,垃圾收集的效果越好,應用程式也會越來越流暢。
3. GC調優3選2原則: 在效能屬性裡面,吞吐量、延遲、記憶體佔用,我們只能選擇其中兩個進行調優,不可三者兼得。
3、效能調優流程
以上就是對應用程式進行jvm調優的基本流程,我們可以看到,jvm調優是根據效能測試結果不斷優化配置而多次迭代的過程。在達到每一個系統需求指標之前,之前的每個步驟都有可能經歷多次迭代。有時候為了達到某一方面的指標,有可能需要對之前的引數進行多次調整,進而需要把之前的所有步驟重新測試一遍。
另外調優一般是從滿足程式的記憶體使用需求開始的,之後是時間延遲的要求,最後才是吞吐量的要求,要基於這個步驟來不斷優化,每一個步驟都是進行下一步的基礎,不可逆行之。以下我們針對每個步驟進行詳細的示例講解。
在JVM的執行模式方面,我們直接選擇server模式,這也是jdk1.6以後官方推薦的模式。
在垃圾收集器方面,我們直接採用了jdk1.6-1.8 中預設的parallel收集器(新生代採用parallelGC,老生代採用parallelOldGC)。
三、確定記憶體佔用
在確定記憶體佔用之前,我們需要知道兩個知識點:
- 應用程式的執行階段
- jvm記憶體分配
1、執行階段
應用程式的執行階段,我可以劃分為以下三個階段:
1、初始化階段 : jvm載入應用程式,初始化應用程式的主要模組和資料。
2、穩定階段:應用在此時運行了大多數時間,經歷過壓力測試的之後,各項效能引數呈穩定狀態。核心函式被執行,已經被jit編譯預熱過。
3、總結階段:最後的總結階段,進行一些基準測試,生成響應的策報告。這個階段我們可以不關注。
確定記憶體佔用以及活躍資料的大小,我們應該是在程式的穩定階段來進行確定,而不是在專案起初階段來進行確定,如何確定,我們先看以下jvm的記憶體分配。
2、jvm記憶體分配&引數
jvm堆中主要的空間,就是以上新生代、老生代、永久代組成,整個堆大小=新生代大小 + 老生代大小 + 永久代大小。 具體的物件提升方式,這裡不再過多介紹了,我們看下一些jvm命令引數,對堆大小的指定。如果不採用以下引數進行指定的話,虛擬機器會自動選擇合適的值,同時也會基於系統的開銷自動調整。
分代 |
引數 |
描述 |
堆大小 |
-Xms |
初始堆大小,預設為實體記憶體的1/64(<1GB) |
-Xmx |
最大堆大小,預設(MaxHeapFreeRatio引數可以調整)空餘堆記憶體大於70%時,JVM會減少堆直到 -Xms的最小限制 |
|
新生代 |
-XX:NewSize |
新生代空間大小初始值 |
-XX:MaxNewSize |
新生代空間大小最大值 |
|
-Xmn |
新生代空間大小,此處的大小是(eden+2 survivor space) |
|
永久代 |
-XX:PermSize |
永久代空間的初始值&最小值 |
-XX:MaxPermSize |
永久代空間的最大值 |
|
老年代 |
老年代的空間大小會根據新生代的大小隱式設定 |
|
初始值=-Xmx減去-XX:NewSize的值 |
||
最小值=-Xmx值減去-XX:MaxNewSize的值 |
在設定的時候,如果關注效能開銷的話,應儘量把永久代的初始值與最大值設定為同一值,因為永久代的大小調整需要進行FullGC 才能實現。
3、計算活躍資料大小
計算活躍資料大小應該遵循以下流程:
如前所述,活躍資料應該是基於應用程式穩定階段時,觀察長期存活與物件在java堆中佔用的空間大小。
計算活躍資料時應該確保以下條件發生:
1.測試時,啟動引數採用jvm預設引數,不人為設定。
2.確保Full GC 發生時,應用程式正處於穩定階段。
採用jvm預設引數啟動,是為了觀察應用程式在穩定階段的所需要的記憶體使用。
如何才算穩定階段?
一定得需要產生足夠的壓力,找到應用程式和生產環境高峰符合狀態類似的負荷,在此之後達到峰值之後,保持一個穩定的狀態,才算是一個穩定階段。所以要達到穩定階段,壓力測試是必不可少的,具體如何如何對應用壓力測試,本篇不過多說明,後期會有專門介紹的篇幅。
在確定了應用出於穩定階段的時候,要注意觀察應用的GC日誌,特別是Full GC 日誌。
GC日誌指令: -XX:+PrintGCTimeStamps -XX:+PrintGCDetails -Xloggc:<filename>
GC日誌是收集調優所需資訊的最好途徑,即便是在生產環境,也可以開啟GC日誌來定位問題,開啟GC日誌對效能的影響極小,卻可以提供豐富資料。
必須得有FullGC 日誌,如果沒有的話,可以採用監控工具強制呼叫一次,或者採用以下命令,亦可以觸發
jmap -histo:live pid
在穩定階段觸發了FullGC我們一般會拿到如下資訊:
從以上gc日誌中,我們大概可以分析到,在發生fullGC之時,整個應用的堆佔用以及GC時間,當然了,為了更加精確,應該多收集幾次,獲取一個平均值。或者是採用耗時最長的一次FullGC來進行估算。
在上圖中,fullGC之後,老年代空間佔用在93168kb(約93MB),我們以此定為老年代空間的活躍資料。
其他堆空間的分配,基於以下規則來進行。
空間 |
命令引數 |
建議擴大倍數 |
java heap |
-Xms和-Xmx |
3-4倍FullGC後的老年代空間佔用 |
永久代 |
-XX:PermSize -XX:MaxPermSize |
1.2-1.5倍FullGc後的永久帶空間佔用 |
新生代 |
-Xmn |
1-1.5倍FullGC之後的老年代空間佔用 |
老年代 |
2-3倍FullGC後的老年代空間佔用 |
基於以上規則和上圖中的FullGC資訊,我們現在可以規劃的該應用堆空間為:
java 堆空間: 373Mb (=老年代空間93168kb*4)
新生代空間:140Mb(=老年代空間93168kb*1.5)
永久代空間:5Mb(=永久代空間3135kb*1.5)
老年代空間: 233Mb=堆空間-新生代看空間=373Mb-140Mb
對應的應用啟動引數應該為:
java -Xms373m -Xmx373m -Xmn140m -XX:PermSize=5m -XX:MaxPermSize=5m
四、延遲調優
在確定了應用程式的活躍資料大小之後,我們需要再進行延遲性調優,因為對於此時堆記憶體大小,延遲性需求無法達到應用的需要,需要基於應用的情況來進行除錯。
在這一步進行期間,我們可能會再次優化堆大小的配置,評估GC的持續時間和頻率、以及是否需要切換到不同的垃圾收集器上。
1、系統延遲需求
在調優之前,我們需要知道系統的延遲需求是那些,以及對應的延遲可調優指標是那些。
- 應用程式可接受的平均停滯時間: 此時間與測量的Minor GC持續時間進行比較。
- 可接受的Minor GC頻率:Minor GC的頻率與可容忍的值進行比較。
- 可接受的最大停頓時間: 最大停頓時間與最差情況下FullGC的持續時間進行比較。
- 可接受的最大停頓發生的頻率:基本就是FullGC的頻率。
以上中,平均停滯時間和最大停頓時間,對使用者體驗最為重要,可以多關注。
基於以上的要求,我們需要統計以下資料:
- MinorGC的持續時間;
- 統計MinorGC的次數;
- FullGC的最差持續時間;
- 最差情況下,FullGC的頻率;
2、優化新生代的大小
比如如上的gc日誌中,我們可以看到Minor GC的平均持續時間=0.069秒,MinorGC 的頻率為0.389秒一次。
如果,我們系統的設定的平均停滯時間為50ms,當前的69ms明顯是太長了,就需要調整。
我們知道新生代空間越大,Minor GC的GC時間越長,頻率越低。
如果想減少其持續時長,就需要減少其空間大小。
如果想減小其頻率,就需要加大其空間大小。
為了降低改變新生代的大小對其他區域的最小影響。在改變新生代空間大小的時候,儘量保持老年代空間的大小。
比如此次減少了新生代空間10%的大小,應該保持老年代和持代的大小不變化,第一步調優後的引數如下變化:
java -Xms359m -Xmx359m -Xmn126m -XX:PermSize=5m -XX:MaxPermSize=5m
新生代的大小有140m變為126,堆大小順應變化,此時老年代是沒有變化的。
3、優化老年代的大小
同上一步一樣,在優化之前,也需要採集gc日誌的資料。此次我們關注的是FullGC的持續時間和頻率。
上圖中,我們可以看到
FullGC 平均頻率 =5.8s
FullGC 平均持續時間=0.14s
(以上為了測試,真實專案的fullGC 沒有這麼快)
如果沒有FullGC的日誌,有辦法可以評估麼?
我們可以通過物件提升率進行計算。
物件提升率
比如上述中啟動引數中,我們的老年代大小=233Mb。
那麼需要多久才能填滿老年代中這233Mb的空閒空間取決於新生代到老年代的提升率。
每次提升老年代佔用量=每次MinorGC 之後 java堆佔用情況 減去 MinorGC後新生代的空間佔用
物件提升率=平均值(每次提升老年代佔用量) 除以 老年代空間
有了物件提升率,我們就可以算出填充滿老年代空間需要多少次minorGC,大概一次fullGC的時間就可以計算出來了。
比如:
上圖中:
第一次minor GC 之後,老年代空間:13740kb - 13732kb =8kb
第二次minor GC 之後,老年代空間:22394kb - 17905kb =4489kb
第三次minor GC 之後,老年代空間:34739kb - 17917kb =16822kb
第四次minor GC 之後,老年代空間:48143kb - 17913kb =30230kb
第五次minor GC 之後,老年代空間:62112kb - 17917kb =44195kb
老年代每次minorGC提升率
4481kb 第二次和第一次minorGC之間
12333kb 第3次和第2次minorGC之間
13408kb 第4次和第3次minorGC之間
13965kb 第5次和第4次minorGC之間
我們可以測算出:
每次minorGC 的平均提升為12211kb,約為12Mb
上圖中,平均minorGC的頻率為 213ms/次
提升率=12211kb/213ms=57kb/ms
老年代空間233Mb ,佔滿大概需要233*1024/57=4185ms 約為4.185s。
FullGC的預期最差頻率時長可以通過以上兩種方式估算出來,可以調整老年代的大小來調整FullGC的頻率,當然了,如果FullGC持續時間過長,無法達到應用程式的最差延遲要求,就需要切換垃圾處理器了。具體如何切換,下篇再講,比如切換為CMS,針對CMS的調優方式又有會細微的差別。
五、吞吐量調優
經過上述漫長 調優過程,最終來到了調優的最後一步,這一步對上述的結果進行吞吐量測試,並進行微調。
吞吐量調優主要是基於應用程式的吞吐量要求而來的,應用程式應該有一個綜合的吞吐指標,這個指標基於真個應用的需求和測試而衍生出來的。當有應用程式的吞吐量達到或者超過預期的吞吐目標,整個調優過程就可以圓滿結束了。
如果出現調優後依然無法達到應用程式的吞吐目標,需要重新回顧吞吐要求,評估當前吞吐量和目標差距是否巨大,如果在20%左右,可以修改引數,加大記憶體,再次從頭除錯,如果巨大就需要從整個應用層面來考慮,設計以及目標是否一致了,重新評估吞吐目標。
對於垃圾收集器來說,提升吞吐量的效能調優的目標就是就是儘可能避免或者很少發生FullGC 或者Stop-The-World壓縮式垃圾收集(CMS),因為這兩種方式都會造成應用程式吞吐降低。儘量在MinorGC 階段回收更多的物件,避免物件提升過快到老年代。
六、最後
據Plumbr公司對特定垃圾收集器使用情況進行了一次調查研究,研究資料使用了84936個案例。在明確指定垃圾收集器的13%的案例中,併發收集器(CMS)使用次數最多;但大多數案例沒有選擇最佳垃圾收集器。這個比例佔用在87%左右。
JVM調優是一個系統而又複雜的工作,目前jvm下的自動調整已經做的比較優秀,基本的一些初始引數都可以保證一般的應用跑的比較穩定了,對部分團隊來說,程式效能可能優先順序不高,預設垃圾收集器已經夠用了。調優要基於自己的情況而來。