[译]GC专家系列5-Java应用性能优化的原则
原文鏈接:http://www.cubrid.org/blog/dev-platform/the-principles-of-java-application-performance-tuning/
本文是GC專家系列中的第五篇。在第一篇理解Java垃圾回收中我們學(xué)習(xí)了幾種不同的GC算法的處理過程,GC的工作方式,新生代與老年代的區(qū)別。所以,你應(yīng)該已經(jīng)了解了JDK 7中的5種GC類型,以及每種GC對性能的影響。
在第二篇Java垃圾回收的監(jiān)控中介紹了在真實場景中JVM是如何運(yùn)行GC,如何監(jiān)控GC數(shù)據(jù)以及有哪些工具可用來方便進(jìn)行GC監(jiān)控。
在第三篇GC 調(diào)優(yōu)中基于真實案例介紹了可用于GC調(diào)優(yōu)的最佳選項。同時也描述了如何通過降低移動到老年代中對象的數(shù)量來縮短Full GC耗時,以及如何設(shè)置GC類型及內(nèi)存大小。
在第四篇Apache的MaxClients設(shè)置及其對Tomcat Full GC的影響中介紹了Apache對MaxClients選項在系統(tǒng)發(fā)生GC時對整體性能的影響。
在本文中我將會介紹Java應(yīng)用性能優(yōu)化的一般原則。具體來說,我會介紹性能優(yōu)化的必要條件、判斷是否需要優(yōu)化的步驟,同時也會列出在性能優(yōu)化過程中經(jīng)遇到的一些問題。在文章結(jié)尾,我會給你一些在性能優(yōu)化過程中如何做出最優(yōu)決定的建議。
概述
不是每個應(yīng)用都需要優(yōu)化。如果系統(tǒng)的運(yùn)行狀況正如你的期望,你就沒必要花費更多精力在額外的性能提升上。然而,在調(diào)試過程中就期望系統(tǒng)能達(dá)到它的目標(biāo)性能往往會比較困難。這時就需要做系統(tǒng)優(yōu)化的工作了。不管使用哪種語言,性能優(yōu)化都要有較高的專業(yè)技能和高度專注。另外,因為每個應(yīng)用都有自己獨特的操作和不同的資源使用情況,在優(yōu)化兩個不同系統(tǒng)中可能需要使用不同的具體方法。所以與開發(fā)應(yīng)用相比,性能優(yōu)化更需要有扎實的基礎(chǔ)知識,例如需要具有虛擬機(jī)、操作系統(tǒng)甚至計算機(jī)體系結(jié)構(gòu)的相關(guān)知識。基于這些基礎(chǔ),再面對系統(tǒng)進(jìn)行優(yōu)化時,成功的機(jī)率就會更高。
一些Java應(yīng)用的優(yōu)化只需要調(diào)整JVM的選項,例如改變垃圾回收類型,不過有時也是需要去調(diào)整源碼。不管使用哪種方式,你首先都需要去監(jiān)控Java應(yīng)用的執(zhí)行處理過程。基于此,本文主要涵蓋的內(nèi)容如下:
如何監(jiān)控Java應(yīng)用
如何設(shè)置JVM選項
如何判斷是否有必要修改應(yīng)用代碼
Java性能優(yōu)化必備的基礎(chǔ)知識
Java應(yīng)用在JVM中運(yùn)行,因此優(yōu)化Java應(yīng)用,你需要理解JVM的運(yùn)行過程。在前面的文章深入理解JVM你可以找到一些關(guān)于JVM重要概念的介紹。
在本文中關(guān)于JVM運(yùn)行過程的講解著重于垃圾收集(GC)和 Hotspot相關(guān)知識。為了構(gòu)造一個使JVM 運(yùn)行良好的環(huán)境,你需要理解操作如何為進(jìn)程分配資源。所以即便是優(yōu)化Java應(yīng)用,你也需要像熟悉JVM一樣去熟悉操作系統(tǒng)甚至硬件知識。
與Java語言相關(guān)的知識也十分重要。同樣理解鎖和并發(fā)、熟悉類的加載與對象創(chuàng)建都是應(yīng)該具備的技能。
一旦將Java應(yīng)用優(yōu)化付諸行動,你就需要綜合利用上面提到的相關(guān)知識進(jìn)行全面分析。
Java性能優(yōu)化的流程
圖1摘取自Charlie Hunt和Binu John合著的《Java性能》,描述了Java應(yīng)用性能優(yōu)化的處理流程。
圖1: Java應(yīng)用性能優(yōu)化流程
上圖并不是一個一次性流程,在性能優(yōu)化完成之前你可能需要重復(fù)其中的過程。此過程同樣適用于如何選取一個期望的性能指標(biāo)。在優(yōu)化過程中,有時需要降低性能指標(biāo)的預(yù)期值,有時則需要提高性能指標(biāo)的預(yù)期值。
JVM部署模型
JVM部署模型關(guān)系到如何決定是否把應(yīng)用部署到單個或多個JVM上運(yùn)行。這可以從系統(tǒng)的可用性、響應(yīng)速度和可維護(hù)性上來做取舍。即便是決定了使用多個JVM,你也還需要確定在單臺服務(wù)器上運(yùn)行多個JVM或者是每臺服務(wù)器上運(yùn)行一個JVM。例如,對每臺服務(wù)器,你面臨著為單個JVM分配8GB堆內(nèi)存和運(yùn)行4個JVM并為每個JVM分配2GB堆內(nèi)存的選擇。當(dāng)然單臺服務(wù)器運(yùn)行的JVM的數(shù)量也取決于CPU的核數(shù)以及應(yīng)用本身的特點。在對比以上兩個配置的響應(yīng)速度時,具有2GB堆空間的方案可能更有優(yōu)勢,因為使用2GB的堆空間比使用8GB堆空間在Full GC時耗時更短。不過話說回來,使用8GB堆空間卻可以減少Full GC的頻率。另外也可以通過提高應(yīng)用內(nèi)部緩存命中率的方式來提高系統(tǒng)響應(yīng)速度。所以,最終選擇部署模型需要綜合考慮應(yīng)用的特點和所選方案對應(yīng)用帶來的優(yōu)劣對比。
JVM體系結(jié)構(gòu)
選擇JVM時還需要面臨32位JVM和64位JVM。同樣條件下,應(yīng)該優(yōu)化選擇32位JVM,因為32位JVM比64位的表現(xiàn)更優(yōu)。不過32位JVM能使用堆內(nèi)存最大理論值只有4GB。(事實上,32位操作系統(tǒng)和64位操作系統(tǒng)能分配的空間大小都只有2-3GB)。當(dāng)堆空間需求更大時,使用64位JVM會是更好的選擇。
表 1:性能對比
| 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 |
接下來要做的就是運(yùn)行應(yīng)用并衡量其性能。這些過程包括GC調(diào)優(yōu)、調(diào)整操作系統(tǒng)設(shè)置以及修改應(yīng)用代碼。在這些過程中,你需要使用一些系統(tǒng)監(jiān)控工具或者程序分析工具來幫你完成任務(wù)。
值得注意的是為響應(yīng)速度的優(yōu)化和為吞吐量的優(yōu)化途徑可能會截然不同。例如,不時發(fā)生的stop-the-world會降低響應(yīng)速度,而Full GC則會導(dǎo)致單位時間內(nèi)的吞吐量量大幅減少。所以其中必定會有所權(quán)衡。當(dāng)然這些權(quán)衡不只發(fā)生于響應(yīng)速度和呑吐量之間,你可能需要使用更多的CPU資源來減少內(nèi)存使用來以避免響應(yīng)速度或吞吐量的降低。與此相反的場景也同樣會發(fā)生,你需要按一定的優(yōu)先順序來解決。
圖1中的性能優(yōu)化流程圖適用于包括Swing應(yīng)用在內(nèi)的幾乎所有Java應(yīng)用。盡管如此,這個流程并不太適用于我們NHN公司為網(wǎng)絡(luò)服務(wù)編寫服務(wù)器應(yīng)用的場景。下圖2是針對NHN公司并基于圖1制定的一個簡化的處理流程。
圖2:NHN公司的推薦的Java應(yīng)用優(yōu)化過程
上圖中的選擇JVM(Select JVM)是說通常32位JVM就足夠了,除非你需要使用JVM維護(hù)幾個GB的緩存數(shù)據(jù)。
好了,基于圖 2中的流程,你將開始學(xué)到處理每一步中所需應(yīng)對的事情。
JVM選項
我將主要介紹如何為Web應(yīng)用服務(wù)器設(shè)置合適的JVM參數(shù)。盡管不能窮盡所有案例,但最優(yōu)的GC算法,尤其針對Web應(yīng)用,通常是CMS GC,這主要是因為Web應(yīng)用的低延遲要求決定的。當(dāng)然在使用CMS過程中,有時會遇到因為過多的內(nèi)存碎片導(dǎo)致的較長時間的stop-the-world現(xiàn)象發(fā)生。不過這個問題可以通過調(diào)整新生代大小或者碎片比例進(jìn)行優(yōu)化。
設(shè)置新生代大小和設(shè)置整個堆大小一樣重要。最好通過-XX:NewRatio參數(shù)設(shè)置新生代空間與整個堆空間的大小比例,或者通過-XX:NewSize來單獨設(shè)置期望的新生代空間。設(shè)置新生代空間的重要性是因為大多數(shù)對象的存活時間很短。在Web應(yīng)用中,除了緩存之外的大多數(shù)對象,是在與HttpRequest相應(yīng)的HttpResponse創(chuàng)建的時候產(chǎn)生的,而這個過程很少會超過1秒,也就是說其中的對象的生命周期也不會超過1秒。如果新生代空間設(shè)置不夠大,當(dāng)需要創(chuàng)建新對象時,舊的對象就需要移到老年代。老年代的GC開銷卻比新生代GC開銷大得多,因此設(shè)置恰當(dāng)?shù)男律臻g是十分重要的。
盡管如此,如果新生代空間超過一定比例,系統(tǒng)的影響速度將會降低。因為新生代垃圾回收的基本過程就把對象從一個存活區(qū)(Survivor area)復(fù)制到另外一個存活區(qū)。所以像老年代一樣,在新生代執(zhí)行GC過程中也同樣會發(fā)生stop-the-world現(xiàn)象。如果新生代設(shè)置變大,存活區(qū)的空間相應(yīng)也會增加,結(jié)果就是需要復(fù)制的數(shù)據(jù)空間將增加。基于這些特點,根據(jù)操作系統(tǒng)不同,通過NewRatio選項為HotSpot JVM設(shè)置合適的新生代空間是很有必要的。
表2: 不同操作系統(tǒng)與JVM選項的NewRatio默認(rèn)值
| Sparc -server | 2 |
| Sparc -client | 8 |
| x86 -server | 8 |
| x86 -client | 12 |
如果設(shè)置了NewRatio,則將有1/(NewRatio + 1)的堆空間屬于新生代。你會發(fā)現(xiàn)上表中Sparc -server的NewRatio的值非常小,因為當(dāng)使用上面的默認(rèn)值時,Sparc系統(tǒng)是用在比 x86更高端的場景中。因為x86性能的提升,目前使用x86 server也變得更為常見,像Sparc -server一樣設(shè)置NewRatio的值為2或3也更為合理。
除此之外,你也可以使用NewSize和MaxNewSize作為NewRatio的替代使用。新生代空間初始大小由NewSize設(shè)定,并且隨著內(nèi)存消耗,新生代空間最大可擴(kuò)展到MaxNewSize的大小。隨著NewRatio的變化,Eden和Survivor區(qū)域的大小也在發(fā)生變化。正如通過相同-Xms和-Xmx為堆空間設(shè)置固定值,為新生代設(shè)置相同的MaxSize和MaxNewSize也是一個不錯的選擇。
如果同時設(shè)置了NewRatio和NewSize,其中較大的值會起作用。所以當(dāng)一個堆空間創(chuàng)建之后,就可以通過如下公式計算初始新生代空間的大小:
min(MaxNewSize, max(NewSize, heap/(NewRatio?+?1)))不過在優(yōu)化過程中,無乎不可能一下子就為堆大小和新生代大小找到了恰當(dāng)?shù)闹怠;谖以贜HN運(yùn)行Web應(yīng)用程序的經(jīng)驗,我推薦在啟動Java應(yīng)用時使用如下JVM選項。在經(jīng)過對這些選項的性能監(jiān)控結(jié)果分析之后,你會找到更合適的GC算法或選項。
表3:推薦的JVM選項
| 運(yùn)行模式 | -server |
| 堆大小 | 指定相同的-Xms和-Xmx |
| 新生代大小 | -XX:NewRatio: 取值在2-4之間 |
| ? | -XX:NewSize=?,?-XX:MaxNewSize=?。使用NewSize替代NewRatio也是不錯的選擇 |
| 永久代大小 | -XX:PermSize = 256m?-XX:MaxPermSize=256m 把永久代大小設(shè)置為一個運(yùn)行時不會出錯的大小,因為它并不影響系統(tǒng)的性能 |
| GC 日志 | -Xloggc:$CATALANA_HOME/logs/gc.log?-XX:+PrintGCDetails?-XX:+PrintGCDateStamps。輸出GC日志并不明顯影響應(yīng)用性能,因此推薦保留詳細(xì)的GC日志信息。 |
| GC 算法 | -XX:+UseParNewGC?-XX:+CMSParallelRemarkEnabled?-XX:+UseConcMarkSweepGC?-XX:CMSInitiatingOccupancyFraction=75。這只是一個推薦的通用配置。根據(jù)應(yīng)用特點不同,其他配置也許更優(yōu)。 |
| OOM發(fā)生時輸出堆dump | -XX:+HeapDumpOnOutOfMemoryError?-XX:HeapDumpPath=$CATALINA_HOME/logs |
| OOM發(fā)生后的執(zhí)行動作 | -XX:OnOutOfMemoryError=$CATALINA_HOME/bin/stop.sh?或者?-XX:OnOutOfMemoryError=$CATALINA_HOME/bin/restart.sh。OOM之后除了保留堆dump外,根據(jù)管理策略選擇合適的運(yùn)行腳本。 |
衡量應(yīng)用的性能
需要獲取能反映應(yīng)用性能的幾個關(guān)鍵信息如下:
TPS(OPS):這個信息用于從概念上理解應(yīng)用的性能。
Request Per Second(RPS):嚴(yán)格來說,RPS并不同于響應(yīng)速度,但你可以把它理解為響應(yīng)速度。通過RPS,你可以檢查用戶獲取請求結(jié)果所耗費的時間。
RPS 標(biāo)準(zhǔn)偏差(RPS Standard Deviation):如果有可以,盡量保持RPS的穩(wěn)定。如果出現(xiàn)偏差,則需要檢查是否需要做GC優(yōu)化或者是否有內(nèi)部系統(tǒng)問題。
為了獲取盡可能精確的性能結(jié)果,首先要對應(yīng)用進(jìn)行充分的預(yù)熱,待穩(wěn)定之后再開始性能測量,因為這時字節(jié)碼已被HotSpot JIT進(jìn)行了編譯。通常,在使用nGrinder工具做負(fù)載測試時,至少要等系統(tǒng)達(dá)到某個負(fù)載水平10分鐘后再測量系統(tǒng)的實際性能。
在關(guān)鍵點上做優(yōu)化
如果nGrinder的測試結(jié)果滿足預(yù)期,那就不需要對應(yīng)用進(jìn)行優(yōu)化。如果性能遜于預(yù)期,則需要開始優(yōu)化以解決問題。下面通過具體案例來看性能優(yōu)化的方法。
Stop-the-World耗時過長
長時間的stop-the-world通常是由于使用了不恰當(dāng)?shù)腉C選項或者不正確的應(yīng)用實現(xiàn)所致。通常可以通過分析工具(profiler)或者堆dump的結(jié)果判斷導(dǎo)致stop-the-world的原因。也就是說可以通過檢查堆中對象的類型和數(shù)量判斷問題原因。如果有過多非必須對象存在,則需要修改應(yīng)用代碼優(yōu)化實現(xiàn)。如果在創(chuàng)建對象過程中沒有明顯的問題,則需要調(diào)整GC選項。
為了把GC選項調(diào)整到恰當(dāng)?shù)脑O(shè)置,你需要有足夠長時間的GC日志,并從中找出在哪種狀況下出現(xiàn)了stop-the-world。關(guān)于選擇合適GC選項的具體細(xì)節(jié),可參考Java 垃圾回收的監(jiān)控。
CPU使用率過低
當(dāng)系統(tǒng)發(fā)生阻塞時,TPS和CPU使用率都會降低。問題可能來自于內(nèi)部交互系統(tǒng)或者高并發(fā)。分析這種場景,可以對線程dump的結(jié)果進(jìn)行分析或者使用分析工具(profiler)。線程dump的分析方法可以參考如何分析Java線程Dumps
使用一些商業(yè)分析工具(profiler),你可以得到非常具體的鎖相關(guān)的分析報告。不過,大多數(shù)場景只需要使用jvisualvm中的CPU分析器就可以獲得滿意的結(jié)果。
CPU使用率過高
如果TPS很低,但CPU使用率卻非常高,就通常由于低效率的代碼實現(xiàn)所致。這種場景,也需要通過使用分析器找到瓶頸的位置。可用的分析工具有jvisuavm,Eclipse的TPTP或者使用JProbe。
優(yōu)化的途徑
關(guān)于應(yīng)用優(yōu)化的一些建議途徑如下:
首先,判斷是否有必要做性能優(yōu)化。衡量系統(tǒng)的性能并非易事,任何時候都不能保證你能得到滿意的結(jié)果。所以如果應(yīng)用已經(jīng)達(dá)到了期望的目標(biāo)性能,就沒必要投入精力做額外的優(yōu)化。
問題就在那里,你需要做的是解決它。Pareto 法則同樣適用于性能優(yōu)化。這并不是說一個特定的低性能表現(xiàn)只來源于一個問題,相反,在性能優(yōu)化過程中,更應(yīng)該把精力投入到對性能影響最大的那一點上。所以,當(dāng)解決了最嚴(yán)重的問題后,就可以接著處理其他問題。不過建議是每次只著重解決一個問題。
你可能想到了氣球效應(yīng)。為了實現(xiàn)一個目標(biāo),你需要決定放棄哪些。你可以通過使用緩存來提高響應(yīng)速度,然而隨著緩存的增加,其Full GC所需耗時也將增加。一般來說,如果你想維持少量的內(nèi)存使用,系統(tǒng)的呑吐量和響應(yīng)時間將會受到影響。所以,你要清楚哪些是最重要的,哪些微不足道的。
到目前為止,你已經(jīng)了解了Java應(yīng)用性能優(yōu)化的方法。為了介紹衡量性能的具體過程,我忽略了一些細(xì)節(jié)。盡管如此,我想本文已足夠應(yīng)對Java Web應(yīng)用的大多數(shù)優(yōu)化場景。
總結(jié)
以上是生活随笔為你收集整理的[译]GC专家系列5-Java应用性能优化的原则的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [译]GC专家系列4-Apache的Ma
- 下一篇: java美元兑换,(Java实现) 美元