如何合理的規劃一次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)。

 

三、確定內存佔用

在確定內存佔用之前,我們需要知道兩個知識點:

  1. 應用程序的運行階段
  2. 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下的自動調整已經做的比較優秀,基本的一些初始參數都可以保證一般的應用跑的比較穩定了,對部分團隊來說,程序性能可能優先級不高,默認垃圾收集器已經夠用了。調優要基於自己的情況而來。

-----------------------------------------------------------------------------

想看更多有趣原創的技術文章,掃描關注公衆號。

關注個人成長和遊戲研發,推動國內遊戲社區的成長與進步。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章