Java GC 專家系列5:Java應用效能優化的原則

segmentfault發表於2016-01-28

本文是GC專家系列中的第五篇。在第一篇理解Java垃圾回收中我們學習了幾種不同的GC演算法的處理過程,GC的工作方式,新生代與老年代的區別。所以,你應該已經瞭解了JDK 7中的5種GC型別,以及每種GC對效能的影響。

在第二篇Java垃圾回收的監控中介紹了在真實場景中JVM是如何執行GC,如何監控GC資料以及有哪些工具可用來方便進行GC監控。

在第三篇GC 調優中基於真實案例介紹了可用於GC調優的最佳選項。同時也描述瞭如何通過降低移動到老年代中物件的數量來縮短Full GC耗時,以及如何設定GC型別及記憶體大小。

在第四篇 Apache的MaxClients設定及其對Tomcat Full GC的影響 中介紹了Apache對 MaxClients 選項在系統發生GC時對整體效能的影響。

在本文中我將會介紹Java應用效能優化的一般原則。具體來說,我會介紹效能優化的必要條件、判斷是否需要優化的步驟,同時也會列出在效能優化過程中經遇到的一些問題。在文章結尾,我會給你一些在效能優化過程中如何做出最優決定的建議。

概述

不是每個應用都需要優化。如果系統的執行狀況正如你的期望,你就沒必要花費更多精力在額外的效能提升上。然而,在除錯過程中就期望系統能達到它的目標效能往往會比較困難。這時就需要做系統優化的工作了。不管使用哪種語言,效能優化都要有較高的專業技能和高度專注。另外,因為每個應用都有自己獨特的操作和不同的資源使用情況,在優化兩個不同系統中可能需要使用不同的具體方法。所以與開發應用相比,效能優化更需要有紮實的基礎知識,例如需要具有虛擬機器、作業系統甚至計算機體系結構的相關知識。基於這些基礎,再面對系統進行優化時,成功的機率就會更高。

一些Java應用的優化只需要調整JVM的選項,例如改變垃圾回收型別,不過有時也是需要去調整原始碼。不管使用哪種方式,你首先都需要去監控Java應用的執行處理過程。基於此,本文主要涵蓋的內容如下:

  • 如何監控Java應用
  • 如何設定JVM選項
  • 如何判斷是否有必要修改應用程式碼

Java效能優化必備的基礎知識

Java應用在JVM中執行,因此優化Java應用,你需要理解JVM的執行過程。在前面的文章深入理解JVM你可以找到一些關於JVM重要概念的介紹。

在本文中關於JVM執行過程的講解著重於垃圾收集(GC)和 Hotspot相關知識。為了構造一個使JVM 執行良好的環境,你需要理解操作如何為程式分配資源。所以即便是優化Java應用,你也需要像熟悉JVM一樣去熟悉作業系統甚至硬體知識。

與Java語言相關的知識也十分重要。同樣理解鎖和併發、熟悉類的載入與物件建立都是應該具備的技能。

一旦將Java應用優化付諸行動,你就需要綜合利用上面提到的相關知識進行全面分析。

Java效能優化的流程

圖1摘取自Charlie Hunt和Binu John合著的《Java效能》,描述了Java應用效能優化的處理流程。

[譯]GC專家系列5-Java應用效能優化的原則

圖1: Java應用效能優化流程

上圖並不是一個一次性流程,在效能優化完成之前你可能需要重複其中的過程。此過程同樣適用於如何選取一個期望的效能指標。在優化過程中,有時需要降低效能指標的預期值,有時則需要提高效能指標的預期值。

JVM部署模型

JVM部署模型關係到如何決定是否把應用部署到單個或多個JVM上執行。這可以從系統的可用性、響應速度和可維護性上來做取捨。即便是決定了使用多個JVM,你也還需要確定在單臺伺服器上執行多個JVM或者是每臺伺服器上執行一個JVM。例如,對每臺伺服器,你面臨著為單個JVM分配8GB堆記憶體和執行4個JVM併為每個JVM分配2GB堆記憶體的選擇。當然單臺伺服器執行的JVM的數量也取決於CPU的核數以及應用本身的特點。在對比以上兩個配置的響應速度時,具有2GB堆空間的方案可能更有優勢,因為使用2GB的堆空間比使用8GB堆空間在Full GC時耗時更短。不過話說回來,使用8GB堆空間卻可以減少Full GC的頻率。另外也可以通過提高應用內部快取命中率的方式來提高系統響應速度。所以,最終選擇部署模型需要綜合考慮應用的特點和所選方案對應用帶來的優劣對比。

JVM體系結構

選擇JVM時還需要面臨 32位JVM64位JVM 。同樣條件下,應該優化選擇32位JVM,因為32位JVM比64位的表現更優。不過32位JVM能使用堆記憶體最大理論值只有4GB。(事實上,32位作業系統和64位作業系統能分配的空間大小都只有2-3GB)。當堆空間需求更大時,使用64位JVM會是更好的選擇。

表 1:效能對比

Benchmark Time (sec) Factor
C++ Opt 23 1.0x
C++ Dbg 197 8.6x
Java 64-bit 134 5.8x
Java 32-bit 290 12.6x
Java 32-bit GC* 106 4.6x
Java 32-bit SPEC GC* 89 3.7x
Scala 82 3.6x
Scala low-level* 67 2.9x
Scala low-level GC* 58 2.5x
Go 6g 161 7.0x
Go Pro* 126 5.5x

接下來要做的就是執行應用並衡量其效能。這些過程包括GC調優、調整作業系統設定以及修改應用程式碼。在這些過程中,你需要使用一些系統監控工具或者程式分析工具來幫你完成任務。

值得注意的是為響應速度的優化和為吞吐量的優化途徑可能會截然不同。例如,不時發生的stop-the-world會降低響應速度,而Full GC則會導致單位時間內的吞吐量量大幅減少。所以其中必定會有所權衡。當然這些權衡不只發生於響應速度和呑吐量之間,你可能需要使用更多的CPU資源來減少記憶體使用來以避免響應速度或吞吐量的降低。與此相反的場景也同樣會發生,你需要按一定的優先順序來解決。

圖1中的效能優化流程圖適用於包括Swing應用在內的幾乎所有Java應用。儘管如此,這個流程並不太適用於我們NHN公司為網路服務編寫伺服器應用的場景。下 圖2 是針對NHN公司並基於 圖1 制定的一個簡化的處理流程。

[譯]GC專家系列5-Java應用效能優化的原則

圖2:NHN公司的推薦的Java應用優化過程

上圖中的 選擇JVM(Select JVM) 是說通常32位JVM就足夠了,除非你需要使用JVM維護幾個GB的快取資料。

好了,基於 圖 2 中的流程,你將開始學到處理每一步中所需應對的事情。

JVM選項

我將主要介紹如何為Web應用伺服器設定合適的JVM引數。儘管不能窮盡所有案例,但 最優的GC演算法 ,尤其針對Web應用,通常是CMS GC,這主要是因為Web應用的低延遲要求決定的。當然在使用CMS過程中,有時會遇到因為過多的記憶體碎片導致的較長時間的stop-the-world現象發生。不過這個問題可以通過調整新生代大小或者碎片比例進行優化。

設定 新生代大小 和設定 整個堆大小 一樣重要。最好通過 -XX:NewRatio 引數設定新生代空間與整個堆空間的大小比例,或者通過 -XX:NewSize 來單獨設定期望的新生代空間。設定新生代空間的重要性是因為大多數物件的存活時間很短。在Web應用中,除了快取之外的大多數物件,是在與 HttpRequest 相應的 HttpResponse 建立的時候產生的,而這個過程很少會超過1秒,也就是說其中的物件的生命週期也不會超過1秒。如果新生代空間設定不夠大,當需要建立新物件時,舊的物件就需要移到老年代。老年代的GC開銷卻比新生代GC開銷大得多,因此設定恰當的新生代空間是十分重要的。

儘管如此,如果新生代空間超過一定比例,系統的影響速度將會降低。因為新生代垃圾回收的基本過程就把物件從一個存活區(Survivor area)複製到另外一個存活區。所以像老年代一樣,在新生代執行GC過程中也同樣會發生stop-the-world現象。如果新生代設定變大,存活區的空間相應也會增加,結果就是需要複製的資料空間將增加。基於這些特點,根據作業系統不同,通過 NewRatio 選項為HotSpot JVM設定合適的新生代空間是很有必要的。

表2: 不同作業系統與JVM選項的NewRatio預設值

OS and option Default -XX:NewRatio
Sparc -server 2
Sparc -client 8
x86 -server 8
x86 -client 12

如果設定了 NewRatio ,則將有 1/(NewRatio + 1) 的堆空間屬於新生代。你會發現上表中 Sparc -server 的 NewRatio 的值非常小,因為當使用上面的預設值時,Sparc系統是用在比 x86更高階的場景中。因為x86效能的提升,目前使用x86 server也變得更為常見,像 Sparc -server 一樣設定 NewRatio 的值為2或3也更為合理。

除此之外,你也可以使用 NewSize 和 MaxNewSize 作為 NewRatio 的替代使用。新生代空間初始大小由 NewSize 設定,並且隨著記憶體消耗,新生代空間最大可擴充套件到 MaxNewSize 的大小。隨著 NewRatio 的變化,Eden和Survivor區域的大小也在發生變化。正如通過相同 -Xms 和 -Xmx 為堆空間設定固定值,為新生代設定相同的 MaxSize 和 MaxNewSize 也是一個不錯的選擇。

如果同時設定了 NewRatio 和 NewSize ,其中較大的值會起作用。所以當一個堆空間建立之後,就可以通過如下公式計算初始新生代空間的大小:

min(MaxNewSize, max(NewSize, heap/(NewRatio + 1)))

不過在優化過程中,無乎不可能一下子就為堆大小和新生代大小找到了恰當的值。基於我在NHN執行Web應用程式的經驗,我推薦在啟動Java應用時使用如下JVM選項。在經過對這些選項的效能監控結果分析之後,你會找到更合適的GC演算法或選項。

表3:推薦的JVM選項

選項型別 選項
執行模式 -server
堆大小 指定相同的 -Xms 和 -Xmx
新生代大小 -XX:NewRatio : 取值在2-4之間
-XX:NewSize=? , -XX:MaxNewSize=? 。使用 NewSize 替代 NewRatio 也是不錯的選擇
永久代大小 -XX:PermSize = 256m -XX:MaxPermSize=256m 把永久代大小設定為一個執行時不會出錯的大小,因為它並不影響系統的效能
GC 日誌 -Xloggc:$CATALANA_HOME/logs/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps 。輸出GC日誌並不明顯影響應用效能,因此推薦保留詳細的GC日誌資訊。
GC 演算法 -XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 。這只是一個推薦的通用配置。根據應用特點不同,其他配置也許更優。
OOM發生時輸出堆dump -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=$CATALINA_HOME/logs
OOM發生後的執行動作 -XX:OnOutOfMemoryError=$CATALINA_HOME/bin/stop.sh 或者 -XX:OnOutOfMemoryError=$CATALINA_HOME/bin/restart.sh 。OOM之後除了保留堆dump外,根據管理策略選擇合適的執行指令碼。

衡量應用的效能

需要獲取能反映應用效能的幾個關鍵資訊如下:

  • TPS(OPS):這個資訊用於從概念上理解應用的效能。
  • Request Per Second(RPS):嚴格來說,RPS並不同於響應速度,但你可以把它理解為響應速度。通過RPS,你可以檢查使用者獲取請求結果所耗費的時間。
  • RPS 標準偏差(RPS Standard Deviation):如果有可以,儘量保持RPS的穩定。如果出現偏差,則需要檢查是否需要做GC優化或者是否有內部系統問題。

為了獲取儘可能精確的效能結果,首先要對應用進行充分的預熱,待穩定之後再開始效能測量,因為這時位元組碼已被HotSpot JIT進行了編譯。通常,在使用nGrinder工具做負載測試時,至少要等系統達到某個負載水平10分鐘後再測量系統的實際效能。

在關鍵點上做優化

如果nGrinder的測試結果滿足預期,那就不需要對應用進行優化。如果效能遜於預期,則需要開始優化以解決問題。下面通過具體案例來看效能優化的方法。

Stop-the-World耗時過長

長時間的 stop-the-world 通常是由於使用了不恰當的GC選項或者不正確的應用實現所致。通常可以通過分析工具(profiler)或者堆dump的結果判斷導致 stop-the-world 的原因。也就是說可以通過檢查堆中物件的型別和數量判斷問題原因。如果有過多非必須物件存在,則需要修改應用程式碼優化實現。如果在建立物件過程中沒有明顯的問題,則需要調整GC選項。

為了把GC選項調整到恰當的設定,你需要有足夠長時間的GC日誌,並從中找出在哪種狀況下出現了stop-the-world。關於選擇合適GC選項的具體細節,可參考Java 垃圾回收的監控。

CPU使用率過低

當系統發生阻塞時,TPS和CPU使用率都會降低。問題可能來自於內部互動系統或者高併發。分析這種場景,可以對執行緒dump的結果進行分析或者使用分析工具(profiler)。執行緒dump的分析方法可以參考 如何分析Java執行緒Dumps

使用一些商業分析工具(profiler),你可以得到非常具體的鎖相關的分析報告。不過,大多數場景只需要使用jvisualvm中的CPU分析器就可以獲得滿意的結果。

CPU使用率過高

如果TPS很低,但CPU使用率卻非常高,就通常由於低效率的程式碼實現所致。這種場景,也需要通過使用分析器找到瓶頸的位置。可用的分析工具有 jvisuavm ,Eclipse的 TPTP 或者使用 JProbe

優化的途徑

關於應用優化的一些建議途徑如下:

首先,判斷是否有必要做效能優化。衡量系統的效能並非易事,任何時候都不能保證你能得到滿意的結果。所以如果應用已經達到了期望的目標效能,就沒必要投入精力做額外的優化。

問題就在那裡,你需要做的是解決它。 Pareto 法則 同樣適用於效能優化。這並不是說一個特定的低效能表現只來源於一個問題,相反,在效能優化過程中,更應該把精力投入到對效能影響最大的那一點上。所以,當解決了最嚴重的問題後,就可以接著處理其他問題。不過建議是每次只著重解決一個問題。

你可能想到了 氣球效應 。為了實現一個目標,你需要決定放棄哪些。你可以通過使用快取來提高響應速度,然而隨著快取的增加,其Full GC所需耗時也將增加。一般來說,如果你想維持少量的記憶體使用,系統的呑吐量和響應時間將會受到影響。所以,你要清楚哪些是最重要的,哪些微不足道的。

到目前為止,你已經瞭解了Java應用效能優化的方法。為了介紹衡量效能的具體過程,我忽略了一些細節。儘管如此,我想本文已足夠應對Java Web應用的大多數優化場景。

作者:Se Hoon Park,網路平臺開發實驗室高階軟體工程師,NHN公司

相關文章