JVM解讀-效能調優例項
JVM效能調優
1 堆設定調優
年輕代大小選擇
- 響應時間優先的應用:儘可能設大,直到接近系統的最低響應時間限制(根據實際情況選擇)。在此種情況下,年輕代收集發生的頻率也是最小的。同時,減少到達年老代的物件。
- 吞吐量優先的應用:儘可能的設定大,可能到達Gbit的程度。因為對響應時間沒有要求,垃圾收集可以並行進行,一般適合8CPU以上的應用。
通過-XX:NewRadio設定新生代與老年代的大小比例,通過-Xmn來設定新生代的大小。
年老代大小選擇
-
響應時間優先的應用:年老代使用併發收集器,所以其大小需要小心設定,一般要考慮併發會話率和會話持續時間等一些引數。如果堆設定小了,可以會造成記憶體碎片、高回收頻率以及應用暫停而使用傳統的標記清除方式;如果堆大了,則需要較長的收集時間。最優化的方案,一般需要參考以下資料獲得:
- 併發垃圾收集資訊
- 持久代併發收集次數
- 傳統GC資訊
- 花在年輕代和年老代回收上的時間比例
-
吞吐量優先的應用:一般吞吐量優先的應用都有一個很大的年輕代和一個較小的年老代。原因是,這樣可以儘可能回收掉大部分短期物件,減少中期的物件,而年老代盡存放長期存活物件。
-
較小堆引起的碎片問題
因為年老代的併發收集器使用標記、清除演算法,所以不會對堆進行壓縮。當收集器回收時,他會把相鄰的空間進行合併,這樣可以分配給較大的物件。但是,當堆空間較小時,執行一段時間以後,就會出現“碎片”,如果併發收集器找不到足夠的空間,那麼併發收集器將會停止,然後使用傳統的標記、清除方式進行回收。如果出現“碎片”,可能需要進行如下配置:- -XX:+UseCMSCompactAtFullCollection:使用併發收集器時,開啟對年老代的壓縮。
- -XX:CMSFullGCsBeforeCompaction=0:上面配置開啟的情況下,這裡設定多少次Full GC後,對年老代進行壓縮
2 GC策略調優
-
能夠忍受full gc的停頓?
是:選擇throughput
否:如果堆較小,使用CMS或者G1;如果堆較大,選擇G1
-
使用預設配置能達到期望目標嗎?
首先儘量使用預設配置,因為垃圾收集技術在不斷髮展成熟,自動優化大多數的效果是最好的。如果預設配置沒有達到期望,請確認垃圾收集是否是效能瓶頸。如負荷較高的應用,如果垃圾收集上的時間不超過3%,即使進行垃圾回收調優效果也不大。
-
應用的停頓時間和預期的目標接近嗎?
是:調整最大停頓時間設定可能是需要做的
否:需要進行其他調整
如果停頓時間太長,但是吞吐量正常,可以嘗試減少新生代大小(如果是full gc,則減少老年代大小),這樣停頓時間變短,但是單次時間變長
-
GC停頓很短了,但是吞吐量上不去?
增大堆的大小,但是單次停頓時間會加長
-
使用併發收集器,發生了由併發模式失敗引發的full gc?
如果CPU資源充足,可以增加併發GC的執行緒數數
-
使用併發收集器,發生由晉升失敗引起的full gc?
如果是CMS,意味著發生了碎片化,這種情況下:使用跟大的堆;儘早啟動後臺回收
如果堆空間較大,可以選擇使用G1
3 JIT調優
- 一般只需要選擇是使用客戶端版或者伺服器版的JIT編譯器即可。
- 客戶端版的JIT編譯器使用:-client指定,伺服器版的使用:-server。
- 選擇哪種型別一般和硬體的配置相關,當然隨著硬體的發展,也沒有一個確定的標準哪種硬體適合哪種配置。
- 兩種JIT編譯器的區別:
- Client版對於程式碼的編譯早於Server版,也意味著程式碼的執行速度在程式執行早期Client版更快。
- Server版對程式碼的編譯會稍晚一些,這是為了獲取到程式本身的更多資訊,以便編譯得到優化程度更高的程式碼。因為執行在Server上的程式通常都會持續很久。
- Tiered編譯的原理:
- JVM啟動之初使用Client版JIT編譯器
- 當HotSpot形成之後使用Server版JIT編譯器再次編譯
- 在Java 8中,預設使用Tiered編譯方式。
不過在Java7版本之後,一旦開發人員在程式中顯式指定命令“-server”時,預設將會開啟分層編譯(Tiered Compilation)策略,由client編譯器和server編譯器相互協作共同來執行編譯任務。不過在早期版本中,開發人員則只能夠通過命令“-XX:+TieredCompilation”手動開啟分層編譯策略。
- -Xint:完全採用直譯器模式執行程式;
- -Xcomp:完全採用即時編譯器模式執行程式;
- -Xmixed:採用直譯器+即時編譯器的混合模式共同執行程式。
啟動優化
Application | -client | -server | -XX:+TieredCompilation | 類數量 |
---|---|---|---|---|
HelloWorld | 0.08s | 0.08s | 0.08s | Few |
NetBeans | 2.83s | 3.92s | 3.07s | ~10000 |
HelloWorld | 51.5s | 54.0s | 52.0s | ~20000 |
總結
- 當程式的啟動速度越快越好時,使用Client版的JIT編譯器更好。
- 就啟動速度而言,Tiered編譯方式的效能和只使用Client的方式十分接近,因為Tiered編譯本質上也會在啟動是使用Client JIT編譯器。
批處理優化
對於批處理任務,任務量的大小是決定執行時間和使用哪種編譯策略的最重要因素:
Number of Tasks | -client | -server | -XX:+TieredCompilation |
---|---|---|---|
1 | 0.142s | 0.176s | 0.165s |
10 | 0.211s | 0.348s | 0.226s |
100 | 0.454s | 0.674s | 0.472s |
1000 | 2.556s | 2.158s | 1.910s |
10000 | 23.78s | 14.03s | 13.56s |
可以發現幾個結論:
- 當任務數量小的時候,使用Client或者Tiered方式的效能類似,而當任務數量大的時候,使用Tiered會獲得最好的效能,因為它綜合使用了Client和Server兩種編譯器,在程式執行之初,使用Client JIT編譯器得到一部分編譯過的程式碼,在程式“熱點”逐漸形成之後,使用Server JIT編譯器得到高度優化的編譯後代碼。
- Tiered編譯方式的效能總是好於單獨使用Server JIT編譯器。
- Tiered編譯方式在任務量不大的時候,和單獨使用Client JIT編譯器的效能相當。
總結
- 當一段批處理程式需要被執行時,使用不同的策略進行測試,使用速度最快的那一種。
- 對於批處理程式,考慮使用Tiered編譯方式作為預設選項。
長時間執行應用的優化
對於長時間執行的應用,比如Servlet程式等,一般會使用吞吐量來測試它們的效能。 以下的一組資料表示了一個典型的資料獲取程式在使用不同“熱身時間”以及不同編譯策略時,對吞吐量(OPS)的影響(執行時間為60s):
Warm-upPeriod | -client | -server | -XX:+TieredCompilation |
---|---|---|---|
0s | 15.87 | 23.72 | 24.23 |
60s | 16.00 | 23.73 | 24.26 |
300s | 16.85 | 24.42 | 24.43 |
即使當“熱身時間”為0秒,因為執行時間為60秒,所以編譯器也有機會在次期間做出優化。
從上面的資料可以發現的幾個結論:
- 對於典型的資料獲取程式,編譯器對程式碼編譯和優化發生的十分迅速,當“熱身時間”顯著增加時,如從60秒增加到300秒,最後得到的OPS差異並不明顯。
- -server JIT編譯器和Tiered編譯的效能顯著優於-client JIT編譯器。
總結
- 對於長時間執行的應用,總是使用-server JIT編譯器或者Tiered編譯策略。
程式碼快取調優(Tuning the Code Cache)
當JVM對程式碼進行編譯後,被編譯的程式碼以彙編指令的形式存在於程式碼快取中(Code Cache),顯然這個快取區域也是有大小限制的,當此區域被填滿了之後,編譯器就不能夠再編譯其他Java位元組碼了。
Code Cache的最大空間可以通過:-XX:ReservedCodeCacheSize=N來進行設定。
4 JVM執行緒調優
調節執行緒棧大小
通過設定-Xss引數,在記憶體比較稀缺的機器上,可以減少執行緒棧的大小,在32位的JVM上,可以減少執行緒棧大小,可以稍稍增加堆的可用記憶體。每個執行緒預設會開啟1M的堆疊,用於存放棧幀、呼叫引數、區域性變數等,對大多數應用而言這個預設值太了,一般256K就足用。
偏向鎖
使用-XX:UseBiasedLocking選項來禁用偏向鎖,偏向鎖預設開啟。偏向鎖可以提高快取命中率,但是因為偏向鎖也需要一些簿記資訊,有時候效能會更糟,比如使用了某些執行緒池,同步資源或程式碼一直都是多執行緒訪問的,那麼消除偏向鎖這一步驟對你來說就是多餘的。
自旋鎖
使用-XX:UseSpinning引數可以設定自旋鎖是否開啟,但是Java7以後自旋鎖無法禁用。
執行緒優先順序
每個執行緒都可以由開發人員指定優先順序,不過真正執行時的優先順序還取決於作業系統為每個執行緒計算的當前優先順序。開發人員不能依賴執行緒優先順序來影響其效能,如果要提高某些任務的優先順序,就必須使用應用層邏輯來劃分優先順序,可以通過將任務指派給不同執行緒池並修改哪些池子大小來實現。
總結
理解執行緒如何運作,可以獲得很大的效能優勢,不過就執行緒的效能而言,沒有太多可以調優的:可以修改的JVM標識相當少,而且效果不明顯。
5 典型案例
$JAVA_ARGS
.=
"
-Dresin.home=$SERVER_ROOT
-server
-Xmx3000M
-Xms3000M
-Xmn600M
-XX:PermSize=500M
-XX:MaxPermSize=500M
-Xss256K
-XX:+DisableExplicitGC
-XX:SurvivorRatio=1
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
-XX:+CMSParallelRemarkEnabled
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0
-XX:+CMSClassUnloadingEnabled
-XX:LargePageSizeInBytes=128M
-XX:+UseFastAccessorMethods
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=70
-XX:SoftRefLRUPolicyMSPerMB=0
-XX:+PrintClassHistogram
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintHeapAtGC
-Xloggc:log/gc.log
";
說明:
64位jdk參考設定,年老代漲得很慢,CMS執行頻率變小,CMS沒有停滯,也不會有promotion failed問題,記憶