一步步優化JVM六:優化吞吐量

   如果你已經進行完了前面的步驟了,那麼你應該知道這是最後一步了。在這一步裏面,你需要測試應用的吞吐量和爲了更高的吞吐量而優化JVM。

   這一步的輸入就是應用的吞吐量性能要求。應用的吞吐量是在應用層面衡量而不是在JVM層面衡量,因此,應用必須要報告出一些吞吐量指標或者應用的某些操作的吞吐量性能指標。觀察到的吞吐量指標然後用可以用來和應用需要的性能指標進行比較,如果達到或者超過要求,那麼這一步就完成了。如果你需要更好的吞吐量的話,有一些JVM優化可以去做。

   這一步的另外一個輸入就是,有多少內存可以供應用使用,就想前面說的GC最大化內存原則,越多可用的內存,性能就更好。這條原則不僅僅適用於吞吐量優化,同樣適用於延遲優化。

   應用的吞吐量需求可能是無法滿足的。如果是這種情況,那麼就需要重新審視應用吞吐量的需求,應用就需要修改或者改變部署模型。如果上面的一種或者多種情況發生了,那麼你需要重新進行前面的優化步驟。

   在前面的步驟裏面,你可能使用吞吐量垃圾回收器解決了問題(通過-XX:+UseParallelOldGC或者-XX:+UsePrallelGC),或者你調整到併發垃圾回收器(CMS)來解決的問題。如果使用的CMS來解決的問題,下面有一些選項來提升應用的吞吐量,下面詳細介紹。如果是使用的吞吐量垃圾回收器,我們將在CMS之後介紹。
   
   CMS吞吐量優化

   能夠用來提升CMS吞吐量的選項數量有限,下面列出一些可以單獨使用或者聯合使用的選項:

   1、使用一些額外的命令選項,在後面的“額外的性能命令行選項”中詳細介紹。
   2、增加young代的空間大小,增加young代的空間大小,可以減少MinorGC的頻率,就能夠減少在一段時間裏面MinorGC佔用的時間。
   3、增加old代的空間大小,增加old代的空間,可以減少CMS垃圾回收的頻率,減少潛在的碎片,可以減少
stop-the-world垃圾回收。
   4、進一步優化young代堆大小,已經在前面的“優化延遲和響應時間”裏面說過了,以及如何優化eden空間任務後和survivor空間大小以減少對象從young代移動到old也在前面已經說過了。需要注意的是,當優化eden和survivor空間大小的時候考慮到一些權衡。
   5、優化CMS週期的啓動,也在前面說過了。
   
   任何上面提到的優化,或者組合使用上面的選擇,都是減少垃圾回收器佔用CPU時間,把CPU留給應用計算。前面兩種選擇,提供一種可能性來提升吞吐量,但是會有stop-the-world垃圾回收的風險,會增加延遲。

   作爲指導,不考慮CMS,MinorGC的次數應該減少10%,你可能只能降低1%-3%。通常來講,如果只能減少3%甚至更少,那麼能夠提升的吞吐量空間恐怕就有限了。

   吞吐量垃圾回收器優化

   優化吞吐量垃圾回收器的目標是避免FullGC或者理想情況下,避免在穩定狀態下FullGC。這個需要優化對象的歲數,這個可以通過制定survivor空間優化完成。你可以讓eden空間更大,可以減少MinorGC的次數。我知道當對象的任期或者歲數達到一定值的時候就會移動到old代,而這個任期就是對象經歷MinorGC的次數,MinorGC的次數越少,對象任期增長越慢,就有可能被MinorGC回收掉,而不是進入old代。

   使用HotSpot VM的吞吐量垃圾回收器,可以通過-XX:+UseParallelOldGC和-XX:+UsePrallelGC,這樣可以提供最好的吞吐量。吞吐量垃圾回收器利用了一種叫做自適應大小的特性,自適應大小是基於對象的分配和存活率來自動改變eden空間和survivor空間大小,目的是優化對象的歲數分佈。自適應大小的企圖是提供易用性,容易優化JVM,以致於提供可靠的吞吐量。自適應大小在大多數應用下,能夠很好的工作,但是關閉自適應大小以及優化eden空間和survivor空間以及old代空間是一個探索提升應用吞吐量的一種辦法。關閉自適應大小會改變應用的程序的靈活性,尤其是在修改應用程序,以及隨着時間的推移應用的數據發生了變化。

   關閉自適應大小可以使用選項:

   -XX:-UseAdaptiveSizePolicy

   注意在“-XX”後面的“-”表明關閉UseAdapivieSizePolicy提供的特性。只有吞吐量垃圾回收器支持這個選項。在其他的垃圾回收器上使用這個選項是無用的。

   另外一個可選的命令行選項,可以產生關於survivor空間佔用更詳細的信息,關於survivor空間是否溢出,對象是否從young代移動到old代,選項是-XX:+PrintAdaptiveSizePolicy。這個選項最好和-XX:+PrintGCDetails以及-XX:+PrintGCDateStamps或者-XX:+PrintGCTimeStamps一起使用。下面是一個垃圾回收的例子-XX:+PrintGCDateStamps, -XX:PrintGCDetails, -XX:-UseAdaptiveSizePolicy (關閉自適應大小), 以及-XX:+PrintAdaptiveSizePolicy:

   2010-12-16T21:44:11.444-0600:
[GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:
survived: 224408984
promoted: 10904856
overflow: false
[PSYoungGen: 6515579K->219149K(9437184K)]
8946490K->2660709K(13631488K), 0.0725945 secs]
[Times: user=0.56 sys=0.00, real=0.07 secs]

   和以前不同的是,以GCAdaptiveSizePolicy開頭的一些額外信息輸出來了,survived標籤表明“to” survivor空間的對象字節數。在這個例子中,survivor空間佔用量是224408984字節,但是移動到old代的字節數卻有10904856字節。overflow表明young代是否有對象溢出到old代,換句話說,就是表明了“to” survivor是否有足夠的空間來容納從eden空間和“from”survivor空間移動而來的對象。爲了更好的吞吐量,期望在應用處於穩定運行狀態下,survivor空間不要溢出。

   爲了開始優化,你應該關閉自適應大小以及獲取在垃圾回收器日誌裏面額外的survivor空間統計信息,使用這兩個選項-XX:-UseAdaptiveSizePolicy以及-XX:+PrintAdaptiveSizePolicy。這樣提供了一些初始化的信息,以幫助做出優化決定。假如之前使用下面的命令行選項:

 
 -Xmx13g -Xms13g -Xmn4g -XX:SurvivorRatio=6 -XX:+UseParallelOldGC -XX:PrintGCDateStamps 
-XX:+PrintGCDetails


  那麼,就應該如下一組命令行選項,來關閉自適應大小和捕獲survivor空間統計信息:

 
 -Xmx13g -Xms13g -Xmn4g -XX:SurvivorRatio=6
-XX:+UseParallelOldGC -XX:PrintGCDateStamps -XX:+PrintGCDetails
-XX:-UseAdaptiveSizePolicy -XX:+PrintAdaptiveSizePolicy


   首先在應用穩定運行狀態下尋找FullGC信息,包括日期和時間戳可以用來識別出應用是否從啓動狀態進入了穩定狀態。舉例,如果你知道應用啓動需要30秒時間,那麼在應用啓動30秒之後才觀察垃圾回收。

   觀察FullGC信息,你可能會發現有一些短存活時間的對象移動到了old代空間,如果FullGC發生了,首先要確定是old代的空間是FullGC之後存活對象的1.5倍。如果有需要,增加old代的空間來保持1.5倍的指標,這樣,可以保證old代有足夠的空間來處理不在預期內的轉移率(導致短的存活時間的對象移動到old代)或者一些未知的情況——導致了對象的轉移過快,擁有這樣的額外空間,可以延遲甚至可能能夠阻止FullGC的發生。

   在確定了old代有足夠的空間之後,就需要觀察MinorGC的狀況。首先需要觀察survivor空間是否溢出,如果survivor空間溢出了,那麼overflow標籤會是true,否則,overload字段會是false。下面是一個survivor空間溢出的例子:

   2010-12-18T10:12:33.322-0600:
[GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:
survived: 446113911
promoted: 10904856
overflow: true
[PSYoungGen: 6493788K->233888K(9437184K)]
7959281K->2662511K(13631488K), 0.0797732 secs]
[Times: user=0.59 sys=0.00, real=0.08 secs]

   如果survivor空間溢出,對象會再達到任期閥值或者消亡之前被移動到old代。換句話說,對象過快的移動到old代。頻繁的survivor空間溢出會導致FullGC,下面說如何優化survivor。
   
優化survivor空間

   優化survivor空間的目標是保持或者老化短時間存活動的對象在young代中,一直到不得不移動到old代中。開始查看每一個MinorGC,尤其是存活的對象字節數。需要注意一點的是,爲了避免應用啓動的時候對象對後面分析的干擾,可以考慮放棄應用剛進入穩定狀態的前面5到10個MinorGC信息。

   每次MinorGC之後的存活對象數量可以通過-XX:+PrintAdaptiveSizePolicy來查看。在下面的例子中,survivor對象的字節數是224408984。

2010-12-16T21:44:11.444-0600:   
[GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:
survived: 224408984
promoted: 10904856
overflow: false
[PSYoungGen: 6515579K->219149K(9437184K)]
8946490K->2660709K(13631488K), 0.0725945 secs]
[Times: user=0.56 sys=0.00, real=0.07 secs]
[GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:
survived: 224408984
promoted: 10904856
overflow: false
[PSYoungGen: 6515579K->219149K(9437184K)]
8946490K->2660709K(13631488K), 0.0725945 secs]
[Times: user=0.56 sys=0.00, real=0.07 secs]

   使用最大存活對象數量以及知道目標survivor空間的佔用量,你可以決定出最差survivor空間大小,以使得讓對象老化得更加高效。如果目標survivor空間的佔用率沒有通過-XX:TargetSurvivorRatio=<percent>指定,那麼目標survivor空間的佔用率是50%。

   首先爲最差的場景優化survivor空間,這個需要找出在MinorGC之後最大的存活對象數量,注意可以忽略應用進入穩定狀態前面的5到10個MinorGC。可以通過awk或者perl腳本來完成這項工作。

   調整survivor空間的大小,不是僅僅修改survivor空間的大小以使得比存活的對象字節數更大那麼簡單。需要記住的是,如果不增加young代的空間大小,而增加survivor空間的大小,會減少eden空間的大小,這樣會導致頻繁的MinorGC,從而是的對象的老化速度加快,更快的進入old代,又會導致FullGC。所以,需要同步增加young代的空間大小。如果不增加old的空間,那麼就有可能造成頻繁的FullGC甚至內存溢出錯誤。因此,如果有可以獲取的空間,需要同步增加Java堆的空間。

   同樣建議,HotSpot Vm使用默認的目標survivor空間佔用率(50%),如果使用了-XX:TargetSurvivorRatio=<percent>,會使用<percent>作爲MinorGC之後目標survivor空間佔用率。如果survivor空間的佔用率可能超過這個目標值,會在對象達到最大歲數之前把對象移動到old代去。

   通過一個例子詳細說明,考慮用下面的命令選項:
-Xmx13g -Xms13g -Xmn4g -XX:SurvivorRatio=6
-XX:+UseParallelOldGC -XX:-UseAdaptiveSizePolicy
-XX:PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintAdaptiveSizePolicy


   總共的Java堆空間是13g,young代是4g,old代是9g,survivor空間的大小是4g/(6+2)=512M。假如一個應用的存活對象是470M,由於沒有明確指定-XX:TargetSurvivorRatio=<percent>,那麼默認的目標survivor空間佔用率是50%,那麼最小的survivor空間應該是940M,也就是最壞的情況,需要設置940M的survivor空間。

   從上面的例子來看,4g的young代空間被分隔成兩個512M的survivor空間和一個3g的eden空間。剛纔分析的最壞情況分配給survivor的空間是940M,差不多和1g相當。爲了保持對象老化速率,即保持MinorGC的頻率,eden空間需要保持在3g。因此,young代需要給每一個survivor空間1g內存以及3g的eden空間,那麼young代需要增加到5g,也就是說young代需要增加1g空間,需要把-Xmn4g選項改成-Xmn5g選項。比較的理想的情況是,同步把Java堆的空間也增加1g。但是如果內存不夠用,需要保證old代空間大小至少是存活對象的1.5倍。

   假設應用的內存需求滿足,增加survivor空間佔用後的命令選項是:

    
 -Xmx14g -Xms14g -Xmn5g -XX:SurvivorRatio=3
-XX:+UseParallelOldGC -XX:-UseAdaptiveSizePolicy
-XX:PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintAdaptiveSizePolicy


   old空間還是9g,young代的空間是5g,比之前大了1g,eden還是3g,每一個survivor空間是1g。

   你可能需要重複多次設定大小,直到滿足內存佔用的條件下到達吞吐量的峯值。吞吐的峯值一般都是在對象最有效的老化的時候的達到的。

   通常的建議是,吞吐量垃圾回收器的垃圾回收的開銷應該小於5%。如果你只能把這個開銷降低1%甚至更少,你可能需要使用除本章描述之外的特別努力和很大的開銷來優化JVM。
  
優化Parallel GC線程

   吞吐量垃圾回收器的線程數的優化同樣基於有多少應用運行在同一個系統裏面以及硬件平臺。就像前面的“優化CMS”裏面提到的,如果是多個應用運行在同一個系統上面,建議使用比垃圾回收器默認使用的線程數更少的線程數,使用選項是-XX:ParallelGCThreads=<n>.

  另外,由於大量的垃圾回收線程同時執行,垃圾回收可能會嚴重影響其他應用的性能。由於Java 6 Update 23之後,默認的垃圾回收線程是執行Runtime.availableProcessors()獲得的,如果這個方法的返回值小於等於8,那麼就用這個返回值,如果比8更大,那麼就取這個值的5/8。如果運行多個應用,可以根據應用的情況來分配線程數,如果應用的消耗是相當的,那麼就用CPU的內核數除以應用數得到每一個應用可以分配的線程。如果應用的load不相當,那麼就可以根據應用的實際情況來衡量和分配。
   
下一步
   如果你到這一步都還沒有能夠達到吞吐量的要求,那麼可以嘗試後面的“額外的性能選項”,如果還是無法達到,就只能修改應用或者JVM部署結構了。如果進行了修改應用或者修改了部署結構,你需要重新做前面的各個步驟。
   可能會用到的一些邊緣場景,下面一節介紹。
   
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章