在第一篇深入淺出Java垃圾回收機制中,我們已經學習了不同的GC算法流程、GC的工作原理、新生代(Young Generation)和老年代(Old Generation)的概念。你應該了解了JDK7中5種GC類型以及各種類型對應用程序的影響。
在第二篇如何監控Java的垃圾回收中,闡述了JVM是怎樣實際執行垃圾回收的,我們怎樣去監控GC以及哪些工具能讓這個過程更高效。
第三篇如何如何優化Java垃圾回收機制中展示了一些基于真實案例的最佳實踐。同時講解了怎樣盡量少地將對象放入老年代空間(Old Area),避免頻繁地執行完全垃圾回收(Full GC)。還說明了如何設置GC的類型和內存大小。
在第四篇Apache的MaxClients參數詳解及其在Tomcat執行FullGC時的影響中,解釋了MaxClients參數的重要性以及它在垃圾回收過程中對整個系統性能的顯著影響。
第五篇文章將講解Java程序性能調優的原則,尤其是在這個過程中必要的知識以及判斷你的程序是否需要調優。還會介紹調優過程中你可能遇到的問題。本文最后會給出一些建議,依據這些你能在對Java程序調優時做出更好的決策。
概述
并不是每個程序都需要調優。如果一個程序性能表現和預期一樣,你不必付出額外的精力去提高它的性能。然而,在程序調試完成之后,很難馬上就滿足它的性能需求,于是就有了調優這項工作。無論哪種編程語言,對應用程序進行調優都需要豐富的技術知識并且注意力高度集中。另外,你也不應該用相同的方式對兩個程序調優,因為每個程序都有它自己獨特的運作方式和不同的資源使用方式。正因如此,調優比寫程序需要更多基礎知識。例如,你需要熟悉虛擬機、操作系統和計算機架構。而當你面對在這些知識基礎上編寫的程序時,就能成功地對它進行調優。
有時調優Java程序只需要修改JVM參數,比如GC的參數。但也有些時候需要修改程序代碼。無論那種方法,你首先都需要監控執行Java程序的進程。因此本文會講解下面幾個問題:
怎樣監控Java程序?
應該給JVM設置怎樣的參數?
如何確定是否需要修改代碼?
對Java程序進行調優的必要知識
Java程序在Java虛擬機中運行。因此為了進行調優,你需要理解JVM的工作流程。我之前有一篇博文Understanding JVM Internals,將讓你對JVM有深入的了解。
本文中有關JVM運作過程的知識主要關于GC和Hotspot。盡管只有這兩方面的知識可能無法對所有的Java程序進行調優,但是這兩個因素在大多數情況下都影響著Java程序的性能。
值得注意的是,從操作系統的角度來看,JVM也是一個應用程序進程。為了給JVM創造良好的運行環境,你還需要對操作系統分配資源的過程有所了解。這意味著,想要調優Java程序,除了JVM你也應該理解操作系統或者硬件的工作方式。
需要具有的知識還有Java這門語言本身。另外理解鎖和并發、類加載和對象創建都是非常重要的。
當開始調優Java程序時,你應該整合以上各方面的知識來完成工作。
Java程序性能調優的過程
圖1是一張Java程序性能調優的流程圖,摘自由Charlie Hunt和Binu John所著的Java Performance。

JVM分布式模型
JVM分布式模型用于決定是在一個JVM還是多個JVM上執行Java程序。你可以根據其有效性、響應能力和可維護性來進行選擇。當在多臺服務器上運行JVM時,你也可以選擇將多個JVM運行于一臺服務器或者每臺服務器運行一個JVM。例如,對于每臺服務器,你可以運行一個使用8GB堆內存的JVM,也可以運行4個使用2GB的JVM。你理應根據處理器內核的個數還有程序的特性來決定這個數量。當優先考慮響應能力時, 使用2GB的堆內存會優于8GB的,原因是這樣能在更短的時間內完成Full GC。當然,8GB的堆內存可以降低Full GC的頻率。如果你的程序使用了內部緩存,還可以通過增加緩存命中率來提高響應能力。綜上所述,選擇合適的模型需要考慮應用程序的特性,然后在各種模型中 選定一個能夠揚長避短的。
JVM架構
選擇JVM其實就是決定使用32位還是64位的JVM。在相同的條件下,你最好用32位的。因為32位的JVM比64位性能更好。然而,32位 JVM最大支持的堆內存是4GB(無論在32位操作系統還是64位的上,實際可分配的大小都只有2-3GB)。如果需要更大的堆內存,還是用64位的 JVM比較合適。
表1:性能比較(數據來源)

下一步就是運行程序來測試它的性能。這個過程包括GC調優、改變操作系統設置和修改代碼。對于這些工作,你可以使用系統監視工具或者性能分析工具。
注意:針對響應能力的調優和針對吞吐量的調優可能使用不同的方法。如果經常性地發生stop-the-word(串行GC暫時中斷程序執行),程序的響應能力就會被降低。比如在高吞吐量時執行Full GC。不要忘記,在調優時往往有得有失。這樣需要折衷處理的事情不僅發生在響應能力和吞吐量之間。例如使用更多的CPU資源來降低內存的使用,或者不得不忍受響應能力和吞吐量其中一個性能指標的下降。相反的情況同樣可能發生,實際的調優應該根據各指標的優先級來執行。
JVM參數
我會主要講解如何為Web服務端程序設置合適的JVM參數。盡管不一定適合所有的案例,但是最好的GC算法是Concurrent Mark Sweep(CMS垃圾回收),特別是對于Web服務端程序。因為低延遲是非常重要的。當然,在使用CMS時,由于新生代空間(New Area)的分配,可能發生較長時間的stop-the-world現象,不過調整新生代空間的大小或者它和整個堆空間的比例可能解決這個問題。
指定新生代空間的大小和指定整個對堆內存的大小同樣重要。你最好使用–XX:NewRatio來指定新生代和整個堆的大小比例,或者直接用–XX:NewSize來指定所需的新生代空間。這個配置是非常必要的,因為大部分對象都不會存活很久。在Web程序中,除了緩存數據,其他多數對象都只在HttpRequest到HttpResponse期間創建。這個時間幾乎不會超過1秒,表示這些對象的存活時間也不會超過1秒。如果新生代空間不夠大,對象會被轉移到老年代空間,以便騰出地方給新對象使用。老年代空間(Old Area)垃圾回收的代價是比新生代空間大的多的,因此很需要設置一個充足的新生代空間。
然而,當新生代空間的大小超過一個特定的水平,程序的響應能力會被降低。因為新生代空間的垃圾回收過程,基本上是將數據從一個Survivor Area復制到另外一個(From Space和To Space)。另外,stop-the-world的現象在新生代空間和老年代空間執行垃圾回收時都會發生。如果新生代空間變大,那么Survivor Area的空間也會更大,于是每次復制的數據就更多。基于這樣一種特性,我們應該通過指定不同操作系統中HotSpot JVM的NewRatio參數來分配合適大小的新生代空間。
表2:不同操作系統和配置下NewRatio的默認值

如果設置了NewRatio,那么整個堆空間的1/(NewRatio +1)就是新生代空間的大小。上表可以看出Sparc -server的NewRatio默認值很小,因為相比x86的操作系統,Sparc以前更多用于高端應用,這個值就是為它們設置的。但現在x86操作系統的性能有很大提升,使用它們作為服務器已經很普遍了。因此指定NewRatio為2或者3是更好的選擇,就和Sparc -server上的配置一樣。
另外,你還可以通過指定NewSize和MaxNewSize來代替NewRatio。那么新生代空間創建時的大小就是指定的NewSize,隨后可以一直增長到MaxNewSize的值。Eden(新創建對象存放的區域)和Survivor Area兩個區域會隨比例增加。就和你為-Xms(譯者注:原文是-Xs,應該是筆誤)和-Xmx設置相同的值一樣,將MaxSize和 MaxNewSize設置為相同的也是一個好選擇。
如果同時指定了NewRatio和NewSize,你應該使用更大的那個。于是,當堆空間被創建時,你可以用過下面的表達式計算初始新生代空間的大小:
1
min(MaxNewSize, max(NewSize, heap/(NewRatio+1)))
無論如何,僅通過一次嘗試就找到合適的堆空間和新生代空間大小是不可能的。根據我在NHN運行Web服務器的經驗,建議使用下面的JVM參數來運行Java程序。監控在這些參數的條件下程序的性能表現之后,你就能夠選擇更合適的GC算法或者配置。
表3:推薦的JVM參數

測定程序的性能
為了得到程序的性能表現,需要以下這些信息:
系統吞吐量(TPS、OPS):從整體概念上理解程序的性能。
每秒請求數(Request Per Second – RPS):嚴格來說,RPS和單純的響應能力是不同的,但是你可以把它理解為響應能力。通過這個指標,你能夠了解到用戶需要多長時間才能得到請求的結果。
RPS的標準差:如果可能的話,還有必要包括事件的RPS。一旦出現了偏差,你應該檢查GC或者網絡系統。
為了得到更準確的性能表現,你應該等到程序徹底啟動完成后再進行測量,因為字節碼隨后會被HotSpot JIT編譯為本地機器碼。總體來說,需要在程序加載完指定功能后,用nGrinder等工具測試至少10分鐘。
切實地調優
如果nGrinder測試的結果滿足了預期,那么你不需要對程序進行性能調優。如果沒有達到預期結果,你就應該執行調優來解決問題。接下來會通過實例講解方法。
stop-the-world耗時過長
stop-the-world耗時過長可能是由于GC參數不合理或者代碼實現不正確。你可以通過分析工具或堆內存轉儲文件(Heap dump)來定位問題,比如檢查堆內存中對象的類型和數量。如果在其中找到了很多不必要的對象,那么最好去改進代碼。如果沒有發現創建對象的過程中有特別的問題,那么最好單純地修改GC參數。
為了適當地調整GC參數,你需要獲取一段足夠長時間的GC日志,還必須知道哪些情況會導致長時間的stop-the-world。想了解更多關于如何選擇合適的GC參數,可以閱讀我同事的一篇博文:How to Monitor Java Garbage Collection。
CPU使用率過低
當系統發生阻塞,吞吐量和CPU使用率都會降低。這可能是由于網絡系統或者并發的問題。為了解決這個問題,你可以分析線程轉儲信息(Thread dump)或者使用分析工具。閱讀這篇文章可以獲得更多關于線程轉儲分析的知識:How to Analyze Java Thread Dumps。
你可以使用商業的分析工具對線程鎖進行精確的分析,不過大部分時候,只需使用JVisualVM中的CPU分析器,就能獲得足夠的信息。
CPU使用率過高
如果吞吐量很低但是CPU使用率卻很高,很可能是低效率代碼導致的。這種情況下,你應該使用分析工具定位代碼中性能的瓶頸。可使用的工具有:JVisualVM、Eclipse TPTP或者JProbe。
調優方法
建議你使用如下方法對程序進行調優。
首先,檢查性能調優是否必要。測量性能不是一件簡單的工作,你也不能保證每次都獲得滿意的結果。因此如果程序已經滿足預期性能需求,不必在調優上增加額外的投入了。
問題只出在一個地方,你要做的就是去解決掉它。二八定律(Pareto principle)對性能調優同樣適用。這不是說某個模塊的低性能一定只源于一個問題,而是強調我們應該在調優時把注意力放在影響最大的那個問題上。在處理好了最重要的之后,你才應該去解決剩下其他的。也就是建議一次只對一個問題進行修復。
另外需要考慮到氣球效應(Balloon effect),有得必有失。你可以通過使用緩存來提高響應能力,但是當緩存逐漸增大,執行一次Full GC的時間也會更長。一般而言,如果你希望內存使用率比較低,那么吞吐量和響應能力可能都會惡化。因此,要知道什么對自己程序來說最重要的,而哪些又是次要的。
- java
- 設計模式
- 設計模式總覽
- 設計原則
- 工廠方法模式
- 抽象工廠模式
- 單例模式
- 建造者模式
- 原型模式
- 適配器模式
- 裝飾者模式
- 代理模式
- 外觀模式
- 橋接模式
- 組合模式
- 享元模式
- 策略模式
- 模板方法模式
- 觀察者模式
- 迭代子模式
- 責任鏈模式
- 命令模式
- 備忘錄模式
- 狀態模式
- 訪問者模式
- 中介者模式
- 解釋器模式
- 附錄
- JVM相關
- JVM內存結構
- Java虛擬機的內存組成以及堆內存介紹
- Java堆和棧
- 附錄-數據結構的堆棧和內存分配的堆區棧區的區別
- Java內存之Java 堆
- Java內存之虛擬機和內存區域概述
- Java 內存之方法區和運行時常量池
- Java 內存之直接內存(堆外內存)
- JAVA內存模型
- Java內存模型介紹
- 內存模型如何解決緩存一致性問題
- 深入理解Java內存模型——基礎
- 深入理解Java內存模型——重排序
- 深入理解Java內存模型——順序一致性
- 深入理解Java內存模型——volatile
- 深入理解Java內存模型——鎖
- 深入理解Java內存模型——final
- 深入理解Java內存模型——總結
- 內存可見性
- JAVA對象模型
- JVM內存結構 VS Java內存模型 VS Java對象模型
- Java的對象模型
- Java的對象頭
- HotSpot虛擬機
- HotSpot虛擬機對象探秘
- 深入分析Java的編譯原理
- Java虛擬機的鎖優化技術
- 對象和數組并不是都在堆上分配內存的
- 垃圾回收
- JVM內存管理及垃圾回收
- JVM 垃圾回收器工作原理及使用實例介紹
- JVM內存回收理論與實現(對象存活的判定)
- JVM參數及調優
- CMS GC日志分析
- JVM實用參數(一)JVM類型以及編譯器模式
- JVM實用參數(二)參數分類和即時(JIT)編譯器診斷
- JVM實用參數(三)打印所有XX參數及值
- JVM實用參數(四)內存調優
- JVM實用參數(五)新生代垃圾回收
- JVM實用參數(六) 吞吐量收集器
- JVM實用參數(七)CMS收集器
- JVM實用參數(八)GC日志
- Java性能調優原則
- JVM 優化經驗總結
- 面試題整理
- 面試題1
- java日志規約
- Spring安全
- OAtuth2.0簡介
- Spring Session 簡介(一)
- Spring Session 簡介(二)
- Spring Session 簡介(三)
- Spring Security 簡介(一)
- Spring Security 簡介(二)
- Spring Security 簡介(三)
- Spring Security 簡介(四)
- Spring Security 簡介(五)
- Spring Security Oauth2 (一)
- Spring Security Oauth2 (二)
- Spring Security Oauth2 (三)
- SpringBoot
- Shiro
- Shiro和Spring Security對比
- Shiro簡介
- Session、Cookie和Cache
- Web Socket
- Spring WebFlux