成为Java GC专家(5)—Java性能调优原则
2019獨(dú)角獸企業(yè)重金招聘Python工程師標(biāo)準(zhǔn)>>>
這是“成為Java GC專家”系列的第五篇文章。在第一篇深入淺出Java垃圾回收機(jī)制中,我們已經(jīng)學(xué)習(xí)了不同的GC算法流程、GC的工作原理、新生代(Young Generation)和老年代(Old Generation)的概念。你應(yīng)該了解了JDK7中5種GC類型以及各種類型對(duì)應(yīng)用程序的影響。
在第二篇如何監(jiān)控Java的垃圾回收中,闡述了JVM是怎樣實(shí)際執(zhí)行垃圾回收的,我們?cè)鯓尤ケO(jiān)控GC以及哪些工具能讓這個(gè)過程更高效。
第三篇如何如何優(yōu)化Java垃圾回收機(jī)制中展示了一些基于真實(shí)案例的最佳實(shí)踐。同時(shí)講解了怎樣盡量少地將對(duì)象放入老年代空間(Old Area),避免頻繁地執(zhí)行完全垃圾回收(Full GC)。還說明了如何設(shè)置GC的類型和內(nèi)存大小。
在第四篇Apache的MaxClients參數(shù)詳解及其在Tomcat執(zhí)行FullGC時(shí)的影響中,解釋了MaxClients參數(shù)的重要性以及它在垃圾回收過程中對(duì)整個(gè)系統(tǒng)性能的顯著影響。
第五篇文章將講解Java程序性能調(diào)優(yōu)的原則,尤其是在這個(gè)過程中必要的知識(shí)以及判斷你的程序是否需要調(diào)優(yōu)。還會(huì)介紹調(diào)優(yōu)過程中你可能遇到的問題。本文最后會(huì)給出一些建議,依據(jù)這些你能在對(duì)Java程序調(diào)優(yōu)時(shí)做出更好的決策。
概述
并不是每個(gè)程序都需要調(diào)優(yōu)。如果一個(gè)程序性能表現(xiàn)和預(yù)期一樣,你不必付出額外的精力去提高它的性能。然而,在程序調(diào)試完成之后,很難馬上就滿足它的 性能需求,于是就有了調(diào)優(yōu)這項(xiàng)工作。無論哪種編程語言,對(duì)應(yīng)用程序進(jìn)行調(diào)優(yōu)都需要豐富的技術(shù)知識(shí)并且注意力高度集中。另外,你也不應(yīng)該用相同的方式對(duì)兩個(gè) 程序調(diào)優(yōu),因?yàn)槊總€(gè)程序都有它自己獨(dú)特的運(yùn)作方式和不同的資源使用方式。正因如此,調(diào)優(yōu)比寫程序需要更多基礎(chǔ)知識(shí)。例如,你需要熟悉虛擬機(jī)、操作系統(tǒng)和計(jì) 算機(jī)架構(gòu)。而當(dāng)你面對(duì)在這些知識(shí)基礎(chǔ)上編寫的程序時(shí),就能成功地對(duì)它進(jìn)行調(diào)優(yōu)。
有時(shí)調(diào)優(yōu)Java程序只需要修改JVM參數(shù),比如GC的參數(shù)。但也有些時(shí)候需要修改程序代碼。無論那種方法,你首先都需要監(jiān)控執(zhí)行Java程序的進(jìn)程。因此本文會(huì)講解下面幾個(gè)問題:
怎樣監(jiān)控Java程序?
應(yīng)該給JVM設(shè)置怎樣的參數(shù)?
如何確定是否需要修改代碼?
對(duì)Java程序進(jìn)行調(diào)優(yōu)的必要知識(shí)
Java程序在Java虛擬機(jī)中運(yùn)行。因此為了進(jìn)行調(diào)優(yōu),你需要理解JVM的工作流程。我之前有一篇博文Understanding JVM Internals,將讓你對(duì)JVM有深入的了解。
本文中有關(guān)JVM運(yùn)作過程的知識(shí)主要關(guān)于GC和Hotspot。盡管只有這兩方面的知識(shí)可能無法對(duì)所有的Java程序進(jìn)行調(diào)優(yōu),但是這兩個(gè)因素在大多數(shù)情況下都影響著Java程序的性能。
值得注意的是,從操作系統(tǒng)的角度來看,JVM也是一個(gè)應(yīng)用程序進(jìn)程。為了給JVM創(chuàng)造良好的運(yùn)行環(huán)境,你還需要對(duì)操作系統(tǒng)分配資源的過程有所了解。這意味著,想要調(diào)優(yōu)Java程序,除了JVM你也應(yīng)該理解操作系統(tǒng)或者硬件的工作方式。
需要具有的知識(shí)還有Java這門語言本身。另外理解鎖和并發(fā)、類加載和對(duì)象創(chuàng)建都是非常重要的。
當(dāng)開始調(diào)優(yōu)Java程序時(shí),你應(yīng)該整合以上各方面的知識(shí)來完成工作。
Java程序性能調(diào)優(yōu)的過程
圖1是一張Java程序性能調(diào)優(yōu)的流程圖,摘自由Charlie Hunt和Binu John所著的Java Performance。
圖1:Java程序性能調(diào)優(yōu)的過程
JVM分布式模型
JVM分布式模型用于決定是在一個(gè)JVM還是多個(gè)JVM上執(zhí)行Java程序。你可以根據(jù)其有效性、響應(yīng)能力和可 維護(hù)性來進(jìn)行選擇。當(dāng)在多臺(tái)服務(wù)器上運(yùn)行JVM時(shí),你也可以選擇將多個(gè)JVM運(yùn)行于一臺(tái)服務(wù)器或者每臺(tái)服務(wù)器運(yùn)行一個(gè)JVM。例如,對(duì)于每臺(tái)服務(wù)器,你可 以運(yùn)行一個(gè)使用8GB堆內(nèi)存的JVM,也可以運(yùn)行4個(gè)使用2GB的JVM。你理應(yīng)根據(jù)處理器內(nèi)核的個(gè)數(shù)還有程序的特性來決定這個(gè)數(shù)量。當(dāng)優(yōu)先考慮響應(yīng)能力 時(shí), 使用2GB的堆內(nèi)存會(huì)優(yōu)于8GB的,原因是這樣能在更短的時(shí)間內(nèi)完成Full GC。當(dāng)然,8GB的堆內(nèi)存可以降低Full GC的頻率。如果你的程序使用了內(nèi)部緩存,還可以通過增加緩存命中率來提高響應(yīng)能力。綜上所述,選擇合適的模型需要考慮應(yīng)用程序的特性,然后在各種模型中 選定一個(gè)能夠揚(yáng)長(zhǎng)避短的。
JVM架構(gòu)
選擇JVM其實(shí)就是決定使用32位還是64位的JVM。在相同的條件下,你最好用32位的。因?yàn)?2位的JVM比64位性能更好。然而,32位 JVM最大支持的堆內(nèi)存是4GB(無論在32位操作系統(tǒng)還是64位的上,實(shí)際可分配的大小都只有2-3GB)。如果需要更大的堆內(nèi)存,還是用64位的 JVM比較合適。
表1:性能比較(數(shù)據(jù)來源)
| 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)行程序來測(cè)試它的性能。這個(gè)過程包括GC調(diào)優(yōu)、改變操作系統(tǒng)設(shè)置和修改代碼。對(duì)于這些工作,你可以使用系統(tǒng)監(jiān)視工具或者性能分析工具。
注意:針對(duì)響應(yīng)能力的調(diào)優(yōu)和針對(duì)吞吐量的調(diào)優(yōu)可能使用不同的方法。如果經(jīng)常性地發(fā)生stop-the-word(串 行GC暫時(shí)中斷程序執(zhí)行),程序的響應(yīng)能力就會(huì)被降低。比如在高吞吐量時(shí)執(zhí)行Full GC。不要忘記,在調(diào)優(yōu)時(shí)往往有得有失。這樣需要折衷處理的事情不僅發(fā)生在響應(yīng)能力和吞吐量之間。例如使用更多的CPU資源來降低內(nèi)存的使用,或者不得不 忍受響應(yīng)能力和吞吐量其中一個(gè)性能指標(biāo)的下降。相反的情況同樣可能發(fā)生,實(shí)際的調(diào)優(yōu)應(yīng)該根據(jù)各指標(biāo)的優(yōu)先級(jí)來執(zhí)行。
上面圖1中的流程展示了幾乎可用于所有Java程序的性能調(diào)優(yōu)過程,包括Swing應(yīng)用。然而,對(duì)于我們公司NHN用于提供網(wǎng)絡(luò)服務(wù)的服務(wù)器端程序來說,這個(gè)方法多少有些不合適。下面圖2中的流程是根據(jù)圖1修改而來,它更簡(jiǎn)單,也更適合NHN。
圖2:對(duì)HNH的Java程序的調(diào)優(yōu)過程
其中,Select JVM表示盡可能使用32位的JVM,除非你需要用64位的JVM來維護(hù)一個(gè)數(shù)GB的緩存。
現(xiàn)在,跟隨圖2中的流程,你會(huì)了解到每一步具體的工作。
JVM參數(shù)
我會(huì)主要講解如何為Web服務(wù)端程序設(shè)置合適的JVM參數(shù)。盡管不一定適合所有的案例,但是最好的GC算法是Concurrent Mark Sweep(CMS垃圾回收),特別是對(duì)于Web服務(wù)端程序。因?yàn)?strong>低延遲是非常重要的。當(dāng)然,在使用CMS時(shí),由于新生代空間(New Area)的分配,可能發(fā)生較長(zhǎng)時(shí)間的stop-the-world現(xiàn)象,不過調(diào)整新生代空間的大小或者它和整個(gè)堆空間的比例可能解決這個(gè)問題。
指定新生代空間的大小和指定整個(gè)對(duì)堆內(nèi)存的大小同樣重要。你最好使用–XX:NewRatio來指定新生代和整個(gè)堆的大小比例,或者直接用–XX:NewSize來指定所需的新生代空間。這個(gè)配置是非常必要的,因?yàn)榇蟛糠謱?duì)象都不會(huì)存活很久。在Web程序中,除了緩存數(shù)據(jù),其他多數(shù)對(duì)象都只在HttpRequest到HttpResponse期間創(chuàng)建。這個(gè)時(shí)間幾乎不會(huì)超過1秒,表示這些對(duì)象的存活時(shí)間也不會(huì)超過1秒。如果新生代空間不夠大,對(duì)象會(huì)被轉(zhuǎn)移到老年代空間,以便騰出地方給新對(duì)象使用。老年代空間(Old Area)垃圾回收的代價(jià)是比新生代空間大的多的,因此很需要設(shè)置一個(gè)充足的新生代空間。
然而,當(dāng)新生代空間的大小超過一個(gè)特定的水平,程序的響應(yīng)能力會(huì)被降低。因?yàn)樾律臻g的垃圾回收過程,基本上是將數(shù)據(jù)從一個(gè)Survivor Area復(fù)制到另外一個(gè)(From Space和To Space)。另外,stop-the-world的現(xiàn)象在新生代空間和老年代空間執(zhí)行垃圾回收時(shí)都會(huì)發(fā)生。如果新生代空間變大,那么Survivor Area的空間也會(huì)更大,于是每次復(fù)制的數(shù)據(jù)就更多。基于這樣一種特性,我們應(yīng)該通過指定不同操作系統(tǒng)中HotSpot JVM的NewRatio參數(shù)來分配合適大小的新生代空間。
表2:不同操作系統(tǒng)和配置下NewRatio的默認(rèn)值
| Sparc -server | 2 |
| Sparc -client | 8 |
| x86 -server | 8 |
| x86 -client | 12 |
如果設(shè)置了NewRatio,那么整個(gè)堆空間的1/(NewRatio +1)就是新生代空間的大小。上表可以看出Sparc -server的NewRatio默認(rèn)值很小,因?yàn)橄啾?strong>x86的操作系統(tǒng),Sparc以前更多用于高端應(yīng)用,這個(gè)值就是為它們?cè)O(shè)置的。但現(xiàn)在x86操作系統(tǒng)的性能有很大提升,使用它們作為服務(wù)器已經(jīng)很普遍了。因此指定NewRatio為2或者3是更好的選擇,就和Sparc -server上的配置一樣。
另外,你還可以通過指定NewSize和MaxNewSize來代替NewRatio。那么新生代空間創(chuàng)建時(shí)的大小就是指定的NewSize,隨后可以一直增長(zhǎng)到MaxNewSize的值。Eden(新創(chuàng)建對(duì)象存放的區(qū)域)和Survivor Area兩個(gè)區(qū)域會(huì)隨比例增加。就和你為-Xms(譯者注:原文是-Xs,應(yīng)該是筆誤)和-Xmx設(shè)置相同的值一樣,將MaxSize和 MaxNewSize設(shè)置為相同的也是一個(gè)好選擇。
如果同時(shí)指定了NewRatio和NewSize,你應(yīng)該使用更大的那個(gè)。于是,當(dāng)堆空間被創(chuàng)建時(shí),你可以用過下面的表達(dá)式計(jì)算初始新生代空間的大小:
| 1 | min(MaxNewSize, max(NewSize, heap/(NewRatio+ ? ? 1 ? ? ))) |
無論如何,僅通過一次嘗試就找到合適的堆空間和新生代空間大小是不可能的。根據(jù)我在NHN運(yùn)行Web服務(wù)器的經(jīng)驗(yàn),建議使用下面的JVM參數(shù)來運(yùn)行Java程序。監(jiān)控在這些參數(shù)的條件下程序的性能表現(xiàn)之后,你就能夠選擇更合適的GC算法或者配置。
表3:推薦的JVM參數(shù)
| 運(yùn)行模式 | -sever |
| 整個(gè)堆內(nèi)存大小 | 為-Xms和-Xmx設(shè)置相同的值。 |
| 新生代空間大小 | -XX:NewRatio: 2到4. -XX:NewSize=? –XX:MaxNewSize=?. 使用NewSize代替NewRatio也是可以的。 |
| 持久代空間大小 | -XX:PermSize=256m -XX:MaxPermSize=256m. 設(shè)置一個(gè)在運(yùn)行中不會(huì)出現(xiàn)問題的值即可,這個(gè)參數(shù)不影響性能。 |
| GC日志 | -Xloggc:$CATALINA_BASE/logs/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps. 記錄GC日志并不會(huì)特別地影響Java程序性能,推薦你盡可能記錄日志。 |
| GC算法 | -XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75. 一般來說推薦使用這些配置,但是根據(jù)程序不同的特性,其他的也有可能更好。 |
| 發(fā)生OOM時(shí)創(chuàng)建堆內(nèi)存轉(zhuǎn)儲(chǔ)文件 | -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=$CATALINA_BASE/logs |
| 發(fā)生OOM后的操作 | -XX:OnOutOfMemoryError=$CATALINA_HOME/bin/stop.sh 或 -XX:OnOutOfMemoryError=$CATALINA_HOME/bin/restart.sh. 記錄內(nèi)存轉(zhuǎn)儲(chǔ)文件后,為了管理的需要執(zhí)行一個(gè)合適的操作。 |
測(cè)定程序的性能
為了得到程序的性能表現(xiàn),需要以下這些信息:
系統(tǒng)吞吐量(TPS、OPS):從整體概念上理解程序的性能。
每秒請(qǐng)求數(shù)(Request Per Second – RPS):嚴(yán)格來說,RPS和單純的響應(yīng)能力是不同的,但是你可以把它理解為響應(yīng)能力。通過這個(gè)指標(biāo),你能夠了解到用戶需要多長(zhǎng)時(shí)間才能得到請(qǐng)求的結(jié)果。
RPS的標(biāo)準(zhǔn)差:如果可能的話,還有必要包括事件的RPS。一旦出現(xiàn)了偏差,你應(yīng)該檢查GC或者網(wǎng)絡(luò)系統(tǒng)。
為了得到更準(zhǔn)確的性能表現(xiàn),你應(yīng)該等到程序徹底啟動(dòng)完成后再進(jìn)行測(cè)量,因?yàn)樽止?jié)碼隨后會(huì)被HotSpot JIT編譯為本地機(jī)器碼。總體來說,需要在程序加載完指定功能后,用nGrinder等工具測(cè)試至少10分鐘。
切實(shí)地調(diào)優(yōu)
如果nGrinder測(cè)試的結(jié)果滿足了預(yù)期,那么你不需要對(duì)程序進(jìn)行性能調(diào)優(yōu)。如果沒有達(dá)到預(yù)期結(jié)果,你就應(yīng)該執(zhí)行調(diào)優(yōu)來解決問題。接下來會(huì)通過實(shí)例講解方法。
stop-the-world耗時(shí)過長(zhǎng)
stop-the-world耗時(shí)過長(zhǎng)可能是由于GC參數(shù)不合理或者代碼實(shí)現(xiàn)不正確。你可以通過分析工具或堆內(nèi)存轉(zhuǎn)儲(chǔ)文件(Heap dump)來定位問題,比如檢查堆內(nèi)存中對(duì)象的類型和數(shù)量。如果在其中找到了很多不必要的對(duì)象,那么最好去改進(jìn)代碼。如果沒有發(fā)現(xiàn)創(chuàng)建對(duì)象的過程中有特別 的問題,那么最好單純地修改GC參數(shù)。
為了適當(dāng)?shù)卣{(diào)整GC參數(shù),你需要獲取一段足夠長(zhǎng)時(shí)間的GC日志,還必須知道哪些情況會(huì)導(dǎo)致長(zhǎng)時(shí)間的stop-the-world。想了解更多關(guān)于如何選擇合適的GC參數(shù),可以閱讀我同事的一篇博文:How to Monitor Java Garbage Collection。
CPU使用率過低
當(dāng)系統(tǒng)發(fā)生阻塞,吞吐量和CPU使用率都會(huì)降低。這可能是由于網(wǎng)絡(luò)系統(tǒng)或者并發(fā)的問題。為了解決這個(gè)問題,你可以分析線程轉(zhuǎn)儲(chǔ)信息(Thread dump)或者使用分析工具。閱讀這篇文章可以獲得更多關(guān)于線程轉(zhuǎn)儲(chǔ)分析的知識(shí):How to Analyze Java Thread Dumps。
你可以使用商業(yè)的分析工具對(duì)線程鎖進(jìn)行精確的分析,不過大部分時(shí)候,只需使用JVisualVM中的CPU分析器,就能獲得足夠的信息。
CPU使用率過高
如果吞吐量很低但是CPU使用率卻很高,很可能是低效率代碼導(dǎo)致的。這種情況下,你應(yīng)該使用分析工具定位代碼中性能的瓶頸。可使用的工具有:JVisualVM、Eclipse TPTP或者JProbe。
調(diào)優(yōu)方法
建議你使用如下方法對(duì)程序進(jìn)行調(diào)優(yōu)。
首先,檢查性能調(diào)優(yōu)是否必要。測(cè)量性能不是一件簡(jiǎn)單的工作,你也不能保證每次都獲得滿意的結(jié)果。因此如果程序已經(jīng)滿足預(yù)期性能需求,不必在調(diào)優(yōu)上增加額外的投入了。
問題只出在一個(gè)地方,你要做的就是去解決掉它。二八定律(Pareto principle)對(duì)性能調(diào)優(yōu)同樣適用。這不是說某個(gè)模塊的低性能一定只源于一個(gè)問題,而是強(qiáng)調(diào)我們應(yīng)該在調(diào)優(yōu)時(shí)把注意力放在影響最大的那個(gè)問題上。在處理好了最重要的之后,你才應(yīng)該去解決剩下其他的。也就是建議一次只對(duì)一個(gè)問題進(jìn)行修復(fù)。
另外需要考慮到氣球效應(yīng)(Balloon effect),有得必有失。你可以通過使用緩存來提高響應(yīng)能力,但是當(dāng)緩存逐漸增大,執(zhí)行一次Full GC的時(shí)間也會(huì)更長(zhǎng)。一般而言,如果你希望內(nèi)存使用率比較低,那么吞吐量和響應(yīng)能力可能都會(huì)惡化。因此,要知道什么對(duì)自己程序來說最重要的,而哪些又是次要的。
到此為止,你應(yīng)該已經(jīng)了解了如何對(duì)Java程序進(jìn)行性能調(diào)優(yōu)。為了介紹性能測(cè)定的具體過程,我不得不省略其中一些細(xì)節(jié),不過我認(rèn)為這些也足夠應(yīng)對(duì)大多數(shù)Java Web服務(wù)端程序了。
最后祝調(diào)優(yōu)好運(yùn)!
原文鏈接: dzone ? ?翻譯: ImportNew.com - 蔣 生武
譯文鏈接: http://www.importnew.com/13954.html
本文轉(zhuǎn)自:http://www.importnew.com/13954.html
轉(zhuǎn)載于:https://my.oschina.net/stefanzhlg/blog/352882
總結(jié)
以上是生活随笔為你收集整理的成为Java GC专家(5)—Java性能调优原则的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: windows下的sysprep
- 下一篇: 使用PHP+ajax打造聊天室应用