成爲Java GC專家(5)—Java性能調優原則

這是「成爲Java GC專家」系列的第五篇文章。在第一篇深刻淺出Java垃圾回收機制中,咱們已經學習了不一樣的GC算法流程、GC的工做原理、新生代(Young Generation)和老年代(Old Generation)的概念。你應該瞭解了JDK7中5種GC類型以及各類類型對應用程序的影響。html

在第二篇如何監控Java的垃圾回收中,闡述了JVM是怎樣實際執行垃圾回收的,咱們怎樣去監控GC以及哪些工具能讓這個過程更高效。java

第三篇如何如何優化Java垃圾回收機制中展現了一些基於真實案例的最佳實踐。同時講解了怎樣儘可能少地將對象放入老年代空間(Old Area),避免頻繁地執行徹底垃圾回收(Full GC)。還說明了如何設置GC的類型和內存大小。算法

在第四篇Apache的MaxClients參數詳解及其在Tomcat執行FullGC時的影響中,解釋了MaxClients參數的重要性以及它在垃圾回收過程當中對整個系統性能的顯著影響。編程

第五篇文章將講解Java程序性能調優的原則,尤爲是在這個過程當中必要的知識以及判斷你的程序是否須要調優。還會介紹調優過程當中你可能遇到的問題。本文最後會給出一些建議,依據這些你能在對Java程序調優時作出更好的決策。緩存

概述

並非每一個程序都須要調優。若是一個程序性能表現和預期同樣,你沒必要付出額外的精力去提升它的性能。然而,在程序調試完成以後,很難立刻就知足它的 性能需求,因而就有了調優這項工做。不管哪一種編程語言,對應用程序進行調優都須要豐富的技術知識而且注意力高度集中。另外,你也不該該用相同的方式對兩個 程序調優,由於每一個程序都有它本身獨特的運做方式和不一樣的資源使用方式。正因如此,調優比寫程序須要更多基礎知識。例如,你須要熟悉虛擬機、操做系統和計 算機架構。而當你面對在這些知識基礎上編寫的程序時,就能成功地對它進行調優。服務器

有時調優Java程序只須要修改JVM參數,好比GC的參數。但也有些時候須要修改程序代碼。不管那種方法,你首先都須要監控執行Java程序的進程。所以本文會講解下面幾個問題:網絡

  • 怎樣監控Java程序?架構

  • 應該給JVM設置怎樣的參數?併發

  • 如何肯定是否須要修改代碼?app

對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

圖1:Java程序性能調優的過程圖1:Java程序性能調優的過程

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:性能比較(數據來源

測試基準 時間(秒) 係數
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-word(串 行GC暫時中斷程序執行),程序的響應能力就會被下降。好比在高吞吐量時執行Full GC。不要忘記,在調優時每每有得有失。這樣須要折衷處理的事情不只發生在響應能力和吞吐量之間。例如使用更多的CPU資源來下降內存的使用,或者不得不 忍受響應能力和吞吐量其中一個性能指標的降低。相反的狀況一樣可能發生,實際的調優應該根據各指標的優先級來執行。

上面圖1中的流程展現了幾乎可用於全部Java程序的性能調優過程,包括Swing應用。然而,對於咱們公司NHN用於提供網絡服務的服務器端程序來講,這個方法多少有些不合適。下面圖2中的流程是根據圖1修改而來,它更簡單,也更適合NHN。

圖2:對HNH的Java程序的調優過程圖2:對HNH的Java程序的調優過程

其中,Select JVM表示儘量使用32位的JVM,除非你須要用64位的JVM來維護一個數GB的緩存。

如今,跟隨圖2中的流程,你會了解到每一步具體的工做。

JVM參數

我會主要講解如何爲Web服務端程序設置合適的JVM參數。儘管不必定適合全部的案例,可是最好的GC算法Concurrent Mark Sweep(CMS垃圾回收),特別是對於Web服務端程序。由於低延遲是很是重要的。固然,在使用CMS時,因爲新生代空間(New Area)的分配,可能發生較長時間的stop-the-world現象,不過調整新生代空間的大小或者它和整個堆空間的比例可能解決這個問題。

指定新生代空間的大小和指定整個對堆內存的大小一樣重要。你最好使用–XX:NewRatio來指定新生代和整個堆的大小比例,或者直接用–XX:NewSize來指定所需的新生代空間。這個配置是很是必要的,由於大部分對象都不會存活好久。在Web程序中,除了緩存數據,其餘多數對象都只在HttpRequestHttpResponse期間建立。這個時間幾乎不會超過1秒,表示這些對象的存活時間也不會超過1秒。若是新生代空間不夠大,對象會被轉移到老年代空間,以便騰出地方給新對象使用。老年代空間(Old Area)垃圾回收的代價是比新生代空間大的多的,所以很須要設置一個充足的新生代空間。

然而,當新生代空間的大小超過一個特定的水平,程序的響應能力會被下降。由於新生代空間的垃圾回收過程,基本上是將數據從一個Survivor Area複製到另一個(From Space和To Space)。另外,stop-the-world的現象在新生代空間和老年代空間執行垃圾回收時都會發生。若是新生代空間變大,那麼Survivor Area的空間也會更大,因而每次複製的數據就更多。基於這樣一種特性,咱們應該經過指定不一樣操做系統中HotSpot JVM的NewRatio參數來分配合適大小的新生代空間。

表2:不一樣操做系統和配置下NewRatio的默認值

操做系統及參數 默認-XX:NewRatio
Sparc -server 2
Sparc -client 8
x86 -server 8
x86 -client 12

若是設置了NewRatio,那麼整個堆空間的1/(NewRatio +1)就是新生代空間的大小。上表能夠看出Sparc -server的NewRatio默認值很小,由於相比x86的操做系統,Sparc之前更多用於高端應用,這個值就是爲它們設置的。但如今x86操做系統的性能有很大提高,使用它們做爲服務器已經很廣泛了。所以指定NewRatio爲2或者3是更好的選擇,就和Sparc -server上的配置同樣。

另外,你還能夠經過指定NewSizeMaxNewSize來代替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參數

類型 參數
運行模式 -sever
整個堆內存大小 爲-Xms和-Xmx設置相同的值。
新生代空間大小 -XX:NewRatio: 2到4. -XX:NewSize=? –XX:MaxNewSize=?. 使用NewSize代替NewRatio也是能夠的。
持久代空間大小 -XX:PermSize=256m -XX:MaxPermSize=256m. 設置一個在運行中不會出現問題的值便可,這個參數不影響性能。
GC日誌 -Xloggc:$CATALINA_BASE/logs/gc.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps. 記錄GC日誌並不會特別地影響Java程序性能,推薦你儘量記錄日誌。
GC算法 -XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75. 通常來講推薦使用這些配置,可是根據程序不一樣的特性,其餘的也有可能更好。
發生OOM時建立堆內存轉儲文件 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=$CATALINA_BASE/logs
發生OOM後的操做 -XX:OnOutOfMemoryError=$CATALINA_HOME/bin/stop.sh 或 -XX:OnOutOfMemoryError=$CATALINA_HOME/bin/restart.sh. 記錄內存轉儲文件後,爲了管理的須要執行一個合適的操做。

測定程序的性能

爲了獲得程序的性能表現,須要如下這些信息:

  • 系統吞吐量(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使用率卻很高,極可能是低效率代碼致使的。這種狀況下,你應該使用分析工具定位代碼中性能的瓶頸。可以使用的工具備:JVisualVMEclipse TPTP或者JProbe

調優方法

建議你使用以下方法對程序進行調優。

首先,檢查性能調優是否必要。測量性能不是一件簡單的工做,你也不能保證每次都得到滿意的結果。所以若是程序已經知足預期性能需求,沒必要在調優上增長額外的投入了。

問題只出在一個地方,你要作的就是去解決掉它。二八定律(Pareto principle)對性能調優一樣適用。這不是說某個模塊的低性能必定只源於一個問題,而是強調咱們應該在調優時把注意力放在影響最大的那個問題上。在處理好了最重要的以後,你才應該去解決剩下其餘的。也就是建議一次只對一個問題進行修復。

另外須要考慮到氣球效應(Balloon effect),有得必有失。你能夠經過使用緩存來提升響應能力,可是當緩存逐漸增大,執行一次Full GC的時間也會更長。通常而言,若是你但願內存使用率比較低,那麼吞吐量和響應能力可能都會惡化。所以,要知道什麼對本身程序來講最重要的,而哪些又是次要的。

到此爲止,你應該已經瞭解瞭如何對Java程序進行性能調優。爲了介紹性能測定的具體過程,我不得不省略其中一些細節,不過我認爲這些也足夠應對大多數Java Web服務端程序了。

最後祝調優好運!

原文連接: dzone    翻譯: ImportNew.com - 蔣 生武
譯文連接: http://www.importnew.com/13954.html

本文轉自:http://www.importnew.com/13954.html

相關文章
相關標籤/搜索