1. 程式人生 > >成為Java GC專家(5)

成為Java GC專家(5)

轉載地址:http://www.importnew.com/13954.html

這是“成為Java GC專家”系列的第五篇文章。在第一篇深入淺出Java垃圾回收機制中,我們已經學習了不同的GC演算法流程、GC的工作原理、新生代(Young Generation)和老年代(Old Generation)的概念。你應該瞭解了JDK7中5種GC型別以及各種型別對應用程式的影響。

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

第三篇如何如何優化Java垃圾回收機制中展示了一些基於真實案例的最佳實踐。同時講解了怎樣儘量少地將物件放入老年代空間(Old Area),避免頻繁地執行完全垃圾回收(Full GC)。還說明了如何設定GC的型別和記憶體大小。

第五篇文章將講解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

圖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程式的調優過程

其中,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使用率卻很高,很可能是低效率程式碼導致的。這種情況下,你應該使用分析工具定位程式碼中效能的瓶頸。可使用的工具有:JVisualVM、 TPTP或者JProbe

調優方法

建議你使用如下方法對程式進行調優。

首先,檢查效能調優是否必要。測量效能不是一件簡單的工作,你也不能保證每次都獲得滿意的結果。因此如果程式已經滿足預期效能需求,不必在調優上增加額外的投入了。

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

另外需要考慮到氣球效應(Balloon effect),有得必有失。你可以通過使用快取來提高響應能力,但是當快取逐漸增大,執行一次Full GC的時間也會更長。一般而言,如果你希望記憶體使用率比較低,那麼吞吐量和響應能力可能都會惡化。因此,要知道什麼對自己程式來說最重要的,而哪些又是次要的。

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

最後祝調優好運!