2014-08-28

Java Performance - Tuning the JVM step by step - 2


Book


Addison Wesley - Java Performance (2012)
Chapter 7 - Tuning the JVM step by step









前情提要
上一篇跑下來,我們首先決定了對系統效能的要求,選擇好JVM的佈屬方式、使用的JVM Runtime。
接著我們首先找出 application的 Live Data Size,根據Live Data Size 調整出初期的記憶體設定
然後就是tuning latency,latency主要是GC造成的,所以tuning的重點在於減少GC的頻率或是持續時間,調整YoungGen的大小來控制 minor GC的頻率與持續時間,然後調整OldGen的大小來控制OldGen/FullGC的頻率,並且在這過程中可能會考慮改為使用CMS GC。


Setp 6: Tune Application Throughput
- Throughput對每個application的定義不同,所以請先修改你的 application或測試程式,讓他們有辦法算出得到throughput。


  • CMS throughput tuning

- 如果在上一步切換成使用CMS GC 的話
CMS GC調整throughput的重點在於降低GC造成的CPU的使用,可能的做法如下:
    - 降低minor GC的頻率 >>> 加大YoungGen
    - 降低CMS Cycle或FullGC的頻率 >>> 加大OldGen或是調整eden或survivor 的空間,讓promote rate下降

* 調整過程,注意所有的GC加起來的時間,1%~3%以內算是很不錯的範圍,最糟不要超過10%



  • Throughput Garbage Collector Tuning

目標:減少FullGC的次數

Throughput GC預設會啟用 "adaptive sizing" ,這功能讓它可以自動調整YoungGen/OldGen的大小,adaptive sizing可以適用大部份的情況,但若想壓榨出更多的throughput,手動調整有可能可以再進一步得到更多的throughput。

-XX:-UseAdaptiveSizePolicy  - 用來停用adaptive sizing(注意 "-Use"代表停用
 - adaptive sizing 預設是啟用的
-XX:+PrintAdaptiveSizePolicy  - 設定印出更多的 survived/promoted/overflow 的資訊

PrintAdaptiveSizePolicy範例:
[GCAdaptiveSizePolicy::compute_survivor_space_size_and_thresh:
   survived: 224408984
   promoted: 10904856
   overflow: false
* 224408984 是 survivor data size, 10904856 是 promoted bytes,false: 沒發生overflow

tuning的方式:

  1. 先找到一個steady state時發生的Full GC
  2. 首先確保OldGen的size 至少要 Live Data Size 的 1.5倍
  3. 接著觀察minor GC,看有沒有 survivor space overflow,如果有的話就加大survivor space的size。接著我們會再探討survivor space的tuning方式


  • Tuning Survivor Spaces

目標:儘可能讓只有需要長期存活的物件被promote到oldGen,減少FullGC


  1. 觀察survivor log,在兩次FullGC中間的minor GC裡找到survived的最大值
  2. 根據找到的survived最大值調整survivor space的大小
    *注意,survivor space的大小改變可能會間接造成eden的改變;若YoungGen的size改變的話,OldGen也會改變,所以調整時要注意它們之間的連動。理想狀態是讓survivor space變大而不改變其他space,但是這會需要更多的記憶體,假設真的沒辦法的話,另一種可能是調整 TargetSurvivorRatio,這樣可以在不改變其他space的size的情況下,讓survivor space有更多可用的空間

範例:
原設定如下:
-Xmx13g -Xms13g -Xmn4g -XX:SurvivorRatio=6 -XX:+UseParallelOldGC -XX:-UseAdaptiveSizePolicy -XX:PrintGCDateStamps -XX:+PrintGCDetails -XX:-PrintAdaptiveSizePolicy
 *以上設定可得:
  heap total 13g   YG = 4g   OG = 13-4 = 9g
  survivor size =  4/(6+2) == 0.5g(each)
  eden size = 4-(0.5*2) = 3g
  預設的 TargetSurvivorRatio == 50%,也就是說survived size 超過256MB就會promote

 若從觀察中發現最大的 survived size 是 473MB
   - 每個 survivor space要到 473 / 50% = 946MB ~= 1G 才會夠
   - 因為eden的大小不想改變,因此YoungGen總大小要變成 5g才夠: -Xmn5g
   - 因為YoungGen總大小變5g了,所以SurvivorRatio要變: -XX:SurvivorRatio=3
     (survivor = 5/(3+2) == 1g, eden = 5-(1*2) == 3g)
   - OldGen size不想改變:  -Xmx14g -Xms14g
新的設定如下:
-Xmx14g -Xms14g -Xmn5g -XX:SurvivorRatio=3 -XX:+UseParallelOldGC -XX:-UseAdaptiveSizePolicy -XX:PrintGCDateStamps -XX:+PrintGCDetails -XX:-PrintAdaptiveSizePolicy
*以上設定假設你有更多的記憶體可以給heap
*若無更多記憶體給heap,另一個方法可以設定 TargetSurvivorRatio == 90%,但請進一步確認不會有突然allocate大量object 的情況,不然可能會造成overflow


  • Tuning Parallel GC Threads
之前有提到可以透過以下設定GC可使用的thread數量
-XX:ParallelGCThreads=<n>  - 設定GC使用的thread數量

ParallelGCThreads預設值受到  Runtime.availableProcessors() 的影響
假設  P = Runtime.availableProcessors()
ParallelGCThreads = P <=8? P: 5/8p

若同一台機器有超過一個application的話,可能會需要設定 ParallelGCThreads,以防止單一個JVM gc影響到同一台機器的其他程式。



  • Deploying on NUMA Systems

若你的JVM運行於支援NUMA的系統上,可以考慮加上   -XX:+UseNUMA  以得到更好的效能
參考

Edge case

某些極端的情況,tuning的方向可能會與前述差很多:


  1. 如果application常常會一次allocate 大量的object,但是卻只有很少的object會長期存活,這種情況適合將YoungGen調大,大於OldGen
  2. 有些application 每次promote都很少,那其實OldGen可以不必比Live Data Size大多少
  3. 有些需要低latency的application,若使用CMS GC,可以考慮將YoungGen調小,讓minor GC更常發生,但是每次minor GC的時間更短,配合大一點的OldGen讓CMS Cycle更少發生,有可能因此latency反而較低

其他額外的Performance相關Command line



  • -XX:+AggressiveOpts

AggressiveOpts這個Option可以用來設定讓JVM使用一些新的performance option,這些option可能還不夠穩定,但是可以得到更好的效能,如果你可以接受一些小風險換來效能的增長的話,可以考慮使用這個Option。

其他可能的設定:
-XX:+DoEscapeAnalysis  - JDK6U23 之後預設啟用
 - escape指的是若某個Object可以給多個thread存取的話,代表這個 object是 escaped
 - Escape analysis可以分析object的使用,如果是從生到死都 non-escaped 的object的話,JVM可以對這個Object的使用做更多的最佳化,以得到更好的效能
-XX:+UseBiasedLocking  - 在 JDK6預設啟用
 - 同一個object會優先被前一個lock此object的thread取用,以降低lock的overhead
-XX:+UseLargePages  - JVM使用 TLB: translation lookaside buffer 來快取 virtual memory與實際 memory的mapping
 - UseLargePages 可以支援更大的TLB,降低TLB missing
- Linux可能不一定支援此Option [參考]
- Windows需要額外的設定才能使用Large Page [參考]
-XX:LargePageSizeInBytes=<n>[g|m|k]  - 在Solaris下,可以使用此Option來指定page size


其他

GCViewer:  可用來幫助分析 GC log  [GCViewer]



2014-08-24

Java Performance - Tuning the JVM step by step - 1


Book


Addison Wesley - Java Performance (2012)
Chapter 7 - Tuning the JVM step by step










開始前

  • 最好可以先解一下第三章的GC或JIT的部份
  • 每個部份的調整可以分開來看,但是了解整個process會有更多幫助



 方法流程
假設一個軟體的執行分成三個phase:

  1. initialization: startup, initial some data structures
  2. steady state: the normal work time
  3. optional phase: maybe generate report or benchmark

其中我們最感興趣的會是 steady state,大部份的調整是針對steady state。

調整過程中,環境與真正production環境越接近,結果越真實。

大至過程如下,過程中若有任何一個phase的目標無法達成,可能會回到上一步或是更早的phase,甚至是重新評估一開始所訂的目標,或是對程式做修改,然後再重覆調整,直到所有的目標都可以滿足。整個過程的圖可以參考圖 7-1 。

  1. Prioritize application systemic requirement: 主要因為不同的需求可能是互斥的,所以會需要先排出優先順序,後續所有的步驟的目標都是要滿足這個需求,如果無法滿足,可能就要修正需求,或是修改程式。
  2. choose JVM deployment model: single JVM or multiple JVM
  3. choose JVM runtime
  4. tuning application memory footprint
  5. tuning latency
  6. tuning throughput

Step 1: Application Systemic Requirements

可能的requirement如下表:
Availabilityapplication持續處於可使用的狀態的能力,一般可以透過將application佈屬至更多的JVM來達成較好的availability,但是如此也會直接地增加management成本。
Manageability維持與監控application的運作所需的人、物力,一般來說,越少的JVM佈屬,越低的管理成本與需求。
Throughput每單位時間可處理的工作量,一般情況,throughput的增加可能會伴隨著latency或memory footprint的增加
Latency and Responsiveness收到request到處理完、回應所需的時間。一般來說,降低latency可能造成throughput的下降,或是增加memory footprint。
Memory Footprint所需的memory用量,增加可用的memory通常可以改善其他的問題,但是memory受限於實體機器的記憶體總量。
Startup Time程式啟始所需要的時間。可能跟起動程式時所需要load的class量、初始化的方式、或是所選的JVM runtime有關…


針對上述requirement根據重要性給予排序,此過程可以邀請較高層級的管理者來參與,以幫助訂出最重要的需求

Step 2: Choose JVM deployment Model
可能的選項: Single JVM、Multiple JVM
Single JVMMultiple JVM
Availability
Manageability成本較低成本相對高
Memory footprint較低的usage overhead每個單一JVM可用的memory量較少
Throughput-scale-out 可能可以增加throughput
Latency-每個單一JVM的heap可能比較小,gc造成的latency有可能會因此降低


Step 3: Choose JVM runtime

  • client or server (or tiered) runtime
client啟動較快,相對較小的記憶體用量,相對較快的JIT(但最佳化的效果相對差)
64bit JVM 沒有client runtime
server啟動較慢,JIT可產生更佳的的結果(但是相對需要更多的時間處理)
tiered號稱結合 client與server runtime的好處,可以考慮用來取代client runtime。
需 J6U25+ or J7
command line: -server-XX:+TieredCompilation



  • 32bit or 64bit runtime
考量的重點:
  1.  64bit JVM 無 client runtime
  2. 所使用的 3rd party 軟體是否可與64bit JVM相容
  3. 所使用的 JNI程式是否有64bit版
  4. 是否需要更多的heap memory?
    根據不同的作業系統,與不同的記憶體需求,表7-1整理了可能的決定方式

  • 決定初始的GC方式
在這個階段會先決定一個初始GC方式,大至來說,書上是直接建議先使用throughput garbage collector (使用 command line option:  -XX:+UseParallelOldGC ),後續的tuning中,也有可能會需要切換成concurrent GC,這會有後面的篇章再詳述。(有關各GC的特性,詳細請參閱CH3)

番外篇: GC Tuning Fundamentals
接下來的Tuning主要是從GC的tuning來調整throughput、latency、memory footprint,加強任何一項都可能使另一項或兩項的效能下降(此時 Step 1 的決定就可以幫助我們做取捨),這邊我們先針對GC Tuning的基本做一些介紹
  • Command Line Options and GC Logging
要tuning GC,看gc log是必需的,gc log需要手動加上 command line options才會輸出,overhead不大但卻可提供有用的資訊,建議即使 Production的環境也將log打開。

指令效果
-Xloggc:<file>指定輸出的log檔案
此為輸出log所需的最小設定之一
-XX:+PrintGCTimeStamps印出log的timestamp
格式是JVM啟動之後所經過的時間(秒)
此為輸出log所需的最小設定之一
-XX:+PrintGCDetails印出GC的統計資訊
此為輸出log所需的最小設定之一
-XX:+PrintGCDateStamps印出日期、時間格式的timestamp
-XX:+PrintGCApplicationStoppedTime輸出GC safepoint的時間
tuning latency 所需
-XX:+PrintGCApplicationConcurrentTime輸出兩次GC safepoint經過的時間
tuning latency 所需
-XX:+PrintSafepointStatistics輸出所有的JVM safepoint事件的資訊,搭配-XX:+PrintGCApplicationStoppedTime可用來濾出GC造成的 safepoint
tuning latency 所需


  • gc log的解讀方式  (接下來的tuning大多需要從GC log了解狀況)
45.152: [GC [PSYoungGen: 295648K->32968K(306432K)] 296198K->33518K(1006848K), 0.1083183 secs] [Times: user=1.83 sys=0.01, real=0.11 secs]

以上log代表
  1. 這個GC是在JVM啟動之後過了  45.152 秒後發生的
  2. 這是個minor GC,其中的PSYoungGen這個gc(這是個Young Generation gc),讓YoungGeneration的space size從 從295648K減少成  32968K,而這個Young Generation的總空間是 306432K (Young Generation的總空間 ==> eden space 加上兩個 survivor space)
  3. 這個JVM的 Young Generation + Old Generation的heap size從總共有 1006848K,並且GC後,size從 296198K 變成 33518K。
  4. 這個GC共花了  0.1083183秒
  5. 這個GC共花了user mode的CPU time約 1.83秒,system mode約0.01秒cpu time,而真實時間約是 0.11秒
 進一步的解讀
  1.  因為eden會在gc後清空,所以可以知道此GC完之後,survivor space的size是 32968K
  2. Old generation 的總空間則是 1106848 - 306432 = 700416
  3. 296198 - 295648 = 550K == GC前Old generation 的size
  4. 33518-32968 = 550K == GC後Old generation的size
  5. 由前兩項可知此次的minor GC,並沒有任何object被promote到 Old Generation
Step 4: Determine Memory Footprint

  • 目標:
計算出此application的 Live Data Size。 Live Data Size指的是在這個application處於Steady state時,一群長期存活著的object 所佔的空間,代表這個application處於steady state時,至少會需要這麼多的記憶體;換個角度說,Live Data Size指的是當application處於steady state時,GC之後的heap size。

  • deploy model的影響:
deploy model會影響JVM可使用的memory量,若選擇 Single JVM的model,那JVM可以使用接近所有的實體記憶體;反之,若選擇多個JVM的佈屬方式,則每個JVM可以使用的實體記憶體會較小


  • 相關的JVM command line指令

指令效果
-Xms

指定 java heap(YoungGen+OldGen)的初始總量
-Xmx指定java heap的最大值,java heap不會超過這個值
-XX:NewSize=<n>[g|m|k]指定java heap的Young Generation的初始大小  ([g|m|k]是單位)
-XX:MaxNewSize=<n>[g|m|k]指定java heap的Young Generation的最大值  ([g|m|k]是單位)
-Xmn<n>[g|m|k]指定java heap的Young Generation的初始值與最大值([g|m|k]是單位)
這個指令等於前兩個指令同時使用並且設定一樣的大小
-XX:PermSize=<n>[g|m|k]指定java Permanent generation的初始大小
([g|m|k]是單位)
-XX:MaxPermSize=<n>[g|m|k]指定java Permanent generation的最大值
([g|m|k]是單位)
*Old Generation的size可以用 java heap的總量減掉 Young Generation的大小計算出來
*一般在Production的環境,-Xmx 與-Xms  會設成一樣大,因為調整heap size需要先經過一次full gc
*一般在Production的環境,-XX:PermSize與-XX:MaxPermSize會設成一樣大,因為調整Permanent Generation 的容量需要先經過一次full gc
*若沒有給定以上任何的指令,則JVM會自動所需的值 [See Ch3 HotSpot VM Adaptive Tuning]

  • 準備開始tuning

  1. 開始時可以先把heap size設大一點,用 -Xms -Xmx 來設定heap size,設定成你認為的application的需求大小,或是也可以不設定,交給jvm自動判斷
  2. 開始時,先用 throughput gc (-XX:+UseParallelOldGC)
  3. 打開 gc log
  4. 你必需有相關的工具讓你可以用產生類似Production環境時的的工作,並且持續運作,直到application運行到進入 steady state。
  5. 如果遇到任何 OutOfMemoryErrors ,找出他的原因(是Old GC fail還是 Perm GC fail?)
  6. 針對OutOfMemoryError的space做加大的動作
  7. 重覆以上動作,直到不會再遇到 OutOfMemoryError
以下為一次GC產生OutOfMemoryError的例子:
2010-11-25T18:51:03.895-0600: [Full GC  [PSYoungGen: 279700K->267300K(358400K)] [ParOldGen: 685165K->685165K(685170K)] 964865K->964865K(1043570K)
[PSPermGen: 32390K->32390K(65536K)], 0.2499342 secs] [Times: user=0.08 sys=0.00, real=0.05 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
從以上的數據我們可以判斷出這是在 Old Gen GC時發生OutOfMemoryError,因為事件發生時,PermGen的size才不到他的limitation的一半,而Old Gen的size則接近他的limitation,而且Old Gen的size在gc完之後機乎沒有變化

  • 計算 Live Data Size
目標
  1. tuning oldGen 的 Live Data Size
  2. tuning PermGen的 Live Data Size
  3. 計算出 full GC造成的 latency
方法
    - 當程式進入 steady state,經過幾次的 full GC,full GC後的 heap size就會是Live Data Size
    - 可以多收集幾次的gc的數據
    - 若GC不常發生,可以透過人工誘發的方式,例如 jconsole/ jvisualVM
    - command line強制gc的方式:  jmap -histo:live <pid>

例子:假設這是app處於steady state後的一次FullGC的結果
[Full GC [PSYoungGen: 273724K->0K(358400K)] [ParOldGen: 426892K->295111K(685170K)] [700616K->295111K(1048570K) [PSPermGen: 32390K->32390K(65536K)], 0.2499342 secs] [Times: user=0.08 sys=0.00 real=0.05 secs]
   - OldGen的 Live Data Size是 295111K(約295MB)
   - PermGen的 Live Data Size是32390K(約32MB)


  •  根據 Live Data Size來調整設定
指令設定
-Xms
-Xmx
3x~4x  OldGen的 Live Data Size
-XX:PermSize -XX:MaxPermSize1.2x~1.5x PermGen 的Live Data Size
-XX:NewSize
-XX:MaxNewSize
-Xmn
約 1x~1.5x OldGen的LiveDataSize
* 因為 -Xms, -Xmx會是3x~4x OldGen LiveDataSize,而YoungGen是 1x~1.5x,所以OldGen差不多會是2x~3x OldGen Live Data Size

範例:
[Full GC [PSYoungGen: 273724K->0K(358400K)] [ParOldGen: 426892K->295111K(685170K)] [700616K->295111K(1048570K) [PSPermGen: 32390K->32390K(65536K)], 0.2499342 secs] [Times: user=0.08 sys=0.00 real=0.05 secs]
   - OldGen的Live Data Size是 295111K(295MB)
   - PermGen的Live Data Size是 32390K(32MB)
   - 可算出 -Xms  -Xmx的建議範圍是 885MB~1180MB,例子中目前是1048MB,在範圍內
   - 算出PermGen的建議值是 38MB~48MB,例子中是64MB,雖然超過建議值,但比起1048MB並不算佔用很大,要不要調整都行
   -  YoungGen的建議大小是 295MB ~ 442MB,目前的設定是 358400K(358MB)
   - 從以上計算的結果,可以得到建議的command line:
-Xms1180m -Xmx1180m -Xmn295m -XX:PermSize=48m -XX:MaxPermSize=48m

  • 其他注意事項
   - heap size並不等於application真正的記憶體用量,例如JNI使用的額外記憶體,或是多thread會需要更多的thread stack…等等
   - 可以使用 OS提供的工具程式來了解實際的記憶體用量
      ex: Solaris 的 "prstat", Linux 的 "top", windows的 "TaskManager"
   - 假設調整完發現機器的實體記憶體無法滿足需求時,可能會需要降低一些其他需求,或是修改程式(可能的做法是做 heap profiling,並且修改程式,降低 Live Data Size)
   - 這邊只是初始的設定,後續的調整也可能會修改到目前的結果

Step 5: Tune Latency/Responsiveness
目標: tuning 由 GC 所造成的 latency

  • 測量
首先先根據上一個步驟的設定結果,做一些GC方面的測量:
  1. minor GC的所需時間
  2. minor GC的頻率
  3. full GC所需時間
  4. full GC的頻率
*1跟2是調整YoungGen的依據
*3跟4可以用來幫間調整OldGen ,也可能會因此換別種GC方式

  • 調整Young Generation GC
   - 主要是根據minor GC的所需時間與頻率來調整
   - 降低YoungGen的大小可以有效減少GC所需的時間,但是會使GC的頻率上升;反之則會增加GC時間,但是降低頻率。
   - 根據所訂的目標latency與發生頻率,重複測試、調整,直到符合需求。
   - 注意,過程中只調整YoungGen的size,old gen的size必需固定。

範例:
假設目標 minor GC所需時間少於 50ms,發生頻率低於 次/5秒
初始時 -Xmn=2048m,-Xms=6144m
範例1: 若minor GC時間是60ms,可考慮減少10% -Xmn,試試 -Xmn=1843m -Xms=5939m
範例2: 若minor GC頻率是 次/2秒,則考慮增加兩倍的-Xmn,用 -Xmn=4096m -Xms=8192m
*以上兩範例都只調整YoungGen的大小,沒有改變OldGen的大小
*反複調整以得到最後的結果
*注意,調整時YoungGen的size請不要低於 -Xms的 10%,否則gc的頻率會太高
*調整時要注意實體記憶體大小,若heap 調太大可能會導至程式運行時會使用到OS的swap,那會使效能大幅下降
*假設最終無法達到需求,那就必需考慮修改程式、改變佈屬的JVM的數量,或是降低需求

  • 調整Old Generation GC
   - 主要根據FullGC的所需時間與頻率來調整
   - 若真的沒有FullGC發生,那就用工具來強制執行FullGC
   - 根據minor GC的統計數據,可以算出每次minor GC平均增加的OldGen heap size,搭配minor GC的頻率與OldGen的size,我們可以預測FullGC的頻率
   - 調整方式類似Young Generation(增加size降低頻率,增加花費時間)
   - 注意此時只調整OldGen的size

若在這個階段,經過調整之後可以達成需求,那就可以跳往下個步驟。
若無法達成需求,那可能可以考慮改成使用 Concurrent GC (CMS)。
接下來會介紹CMS GC的tuning

  • Concurrent Mark Sweep
command line option: -XX:+UseConcMarkSweepGC
   - promote Old Generation較慢,所以如果minor GC需要promote OldGen,會需要較長時間
   - throughput下降,因為在程式執行中會concurrent對OldGen做gc,但因此fullGC頻率會較低
   - 只有在 OldGen不足時,才會做compacting,compacting GC會造成stop-the-world
   - compacting 速度很慢
   - OldGen的size會調高 20%~30%
   - tuning 目標:避免 stop-the-world compacting GC

  • Tuning CMS gc
tuning的重點
  1.  promote rate
  2.  concurrent OldGen gc rate
  3.  fragmentation of OldGen

  • Tuning Promote Rate
Promote Rate主要是Tuning CMS的Young Generation 的size
一般的Promote流程:
  1. 新allocate的Object會放進eden
  2. 若eden滿了會發生 minor GC
  3. minor GC會清空eden與一個survivor space,將生下來的object放在另一個survivor space
  4. 若survivor space不足以放下minor GC的結果(稱為Overflow),則會發生promote OldGen
Promote Rate的調整的目標在於,調整Survivor space的大小,讓Object只有在真的夠老了才被Promote OldGen。

Command Line:  
指令解說
-XX:SurvivorRatio=<ratio>- survivor size == -Xmn<value>/(-XX:SurvivorRatio=<ratio> + 2)
- 所以ratio越大,survivor space越小
- 在其他條件不變的情況下,ratio設定越小,survivor空間越大較不易發生Overflow,但是相對來說,eden越小所以minor GC的機會增加,反而會造成object Age增加的速度變快


- 範例: -Xmn512m -XX:SurvivorRatio=2
        則 eden = 256m, Survivor is 128+128
-XX:TargetSurvivorRatio=<percent>- 預設 50
- 在allocate survivor space時的比例,若50%的話,那survivor就只會有一半的size
- 範例: -Xmn512m -XX:SurvivorRatio=2
        則 eden = 256m, Survivor is 128+128
        但因預設 -XX:TargetSurvivorRatio=50,所以實際運行時survivor space只會有 64mb
-XX:MaxTenuringThreshold=<n>- n= 0-15
- 用來設定 survivor object的max age
- ParNew gc會自己計算 Tenuring Threshold,不管ParNew算出來的Tenuring Threshold多少,object的age都不會超這邊設定的max值
- 當 survivor object的age大於 Tenuring Threshold,就會被promote到 OldGen
- 設成0的話,每次minor GC都會發生Promote
- Tenuring Threshold太大的話,object會存活太久,Overflow的機率增加
-XX:+PrintTenuringDistribution設定輸出 survivor space的資訊


調整範例:
A.
Desired survivor size 8388608 bytes, new threshold 1 (max 15)
  - age 1: 16690480 bytes, 16690480 total
 - ParNew計算的Threshold是1,max設15,Survivor的size是 8MB
 - 若 -XX:TargetSurvivorRatio=50,可推出原設定是 -Xmn512 -XX:SurvivorRatio=30 ((512/32)*0.5 = 8)
 - 此次GC的結果,age 1的 object有 16mb,超出了Survivor的大小,因此此次GC會有overflow產生,會有8MB的object被promote到OldGen
 - 則若要調整讓此次GC不做Promote的話,survivor space的大小就要有 32m(32*0.5=16),那新的設定會是 -Xmn544 -XX:SurvivorRatio=15  (注意,-Xmn變大是因為要保持eden不變)
 - 若heap size不能變的話,那eden就需要變小,那設定會是 Xmn512m -XX:SurvivorRatio=14

B
Desired survivor size 16777216 bytes, new threshold 15 (max 15)
    - age 1: 6115072 bytes, 6115072 total
    - age 2: 286672 bytes, 6401744 total
    - age 3: 115704 bytes, 6517448 total
    - age 4: 95932 bytes, 6613380 total
    - age 5: 89465 bytes, 6702845 total
    - age 6: 88322 bytes, 6791167 total
    - age 7: 88201 bytes, 6879368 total
    - age 8: 88176 bytes, 6967544 total
- 觀察後可發現 age > 3之後沒什麼變化,可以考慮將threshold max設成3或4,提早將object promote到OldGen,減少不必要的Object搬移

調整注意事項
- Promote rate主要是調整YoungGen,但還是要注意,若YoungGen調太大,則minor GC的所需時間可能會上升,若因此造成latency太長,那就必需做出取捨,可能要為了latency調低YoungGen的大小,雖然Promote Rate會因此上升
- 若無法得到滿意的結果,那就必需修改程式或是降低需求或佈屬更多的JVM


  • tuning concurrent OldGen gc rate
 - 主要是要tuning CMS collction Cycle
 - 讓CMS的OldGen space能維持足夠的空間,防止因為OldGen空間不足而造成的Stop-the-world compacting GC。
 - 若collection cycle太慢,則來不及清理OldGen,會造成Stop-the-world compacting GC;反之,若太快,則會造成不必要的效能損失,降低throughput,但通常這總比Stop-the-world好

先產生一次 stop-the-world
這次的tuning從一次stop-the-world gc的發生開始,log如下:
174.445: [GC 174.446: [ParNew: 66408K->66408K(66416K), 0.0000618 secs]174.446: [CMS (concurrent mode failure): 161928K->162118K(175104K), 4.0975124 secs] 228336K->162118K(241520K)
* 注意 concurrent mode failure,CMS GC若出現此訊息代表這是個 stop-the-world GC

調整 CMSInitiatingOccupancyFraction
-XX:CMSInitiatingOccupancyFraction=<percent>- 設定CMS collection cycle的門檻
- 代表當OldGen超過該百分比時,會發生 CMS cycle
- 可以先從 Live Data Size的 1.5倍開始調整
-XX:+UseCMSInitiatingOccupancyOnly- 若沒加上此command的話,CMSInitiatingOccupancyFraction 只會發生一次
* 注意,計算CMSInitiatingOccupancyFraction的比例時需要注意Live Data Size,否則可能會造成一直不斷發生gc
* 若調整時發現算完 CMSInitiatingOccupancyFraction 的空間小於Live Data Size,代表需要增加OldGen的容量
*反複調整 CMSInitiatingOccupancyFraction,直到可以符合需求

範例:
原設定: -Xmx1536m -Xms1536 -Xmn512m
  OldGen size == 1024,推算 Live Data Size約  350m
  CMSInitiatingOccupancyFraction先從1.5倍Live Data Size開始,所以是350*1.5=525m, 525m/1024m = 51%
  結果:-Xmx1536m -Xms1536m -Xmn512m -XX:CMSInitiatingOccupancyFraction=51 -XX:+UseCMSInitiatingOccupancyOnly

範例A:CMS cycle太晚
  [ParNew 742993K->648506K(773376K), 0.1688876 secs]
  [ParNew 753466K->659042K(773376K), 0.1695921 secs]
  [CMS-initial-mark 661142K(773376K), 0.0861029 secs]
  [Full GC 645986K->234335K(655360K), 8.9112629 secs]
  [ParNew 339295K->247490K(773376K), 0.0230993 secs]
  [ParNew 352450K->259959K(773376K), 0.1933945 secs]
* CMS-initial-mark完馬上發生Full GC通常代表 CMS Cycle太晚發生,導至OldGen容量不足


範例B:CMS cycle太早
  [ParNew 390868K->296358K(773376K), 0.1882258 secs]
  [CMS-initial-mark 298458K(773376K), 0.0847541 secs]
  [ParNew 401318K->306863K(773376K), 0.1933159 secs]
  [CMS-concurrent-mark: 0.787/0.981 secs]
  [CMS-concurrent-preclean: 0.149/0.152 secs]
  [CMS-concurrent-abortable-preclean: 0.105/0.183 secs]
  [CMS-remark 374049K(773376K), 0.0353394 secs]
  [ParNew 407285K->312829K(773376K), 0.1969370 secs]
  [ParNew 405554K->311100K(773376K), 0.1922082 secs]
  [ParNew 404913K->310361K(773376K), 0.1909849 secs]
  [ParNew 406005K->311878K(773376K), 0.2012884 secs]
  [CMS-concurrent-sweep: 2.179/2.963 secs]
  [CMS-concurrent-reset: 0.010/0.010 secs]
* 可以發現CMS-initial-mark跟 CMS-concurrent-reset中間的ParNew幾乎沒變化,這代表CMS cycle太常發生
* 這個範例中,從 CMS-initial-mark 298458K(773376K) 可知道 CMSInitiatingOccupancyFraction 約是 35%~40% (因為 298458/773376 = 38.5),我們可以將 CMSInitiatingOccupancyFraction設高點(ex: 50%),來減慢CMS Cycle的週期

範例C: 一個正常運作的CMS GC回收了大量object而不必stop-the-world
  [ParNew 640710K->546360K(773376K), 0.1839508 secs]
  [CMS-initial-mark 548460K(773376K), 0.0883685 secs]
  [ParNew 651320K->556690K(773376K), 0.2052309 secs]
  [CMS-concurrent-mark: 0.832/1.038 secs]
  [CMS-concurrent-preclean: 0.146/0.151 secs]
  [CMS-concurrent-abortable-preclean: 0.181/0.181 secs]
  [CMS-remark 623877K(773376K), 0.0328863 secs]
  [ParNew 655656K->561336K(773376K), 0.2088224 secs]
  [ParNew 648882K->554390K(773376K), 0.2053158 secs]
  [ParNew 489586K->395012K(773376K), 0.2050494 secs]
  [ParNew 463096K->368901K(773376K), 0.2137257 secs]
  [CMS-concurrent-sweep: 4.873/6.745 secs]
  [CMS-concurrent-reset: 0.010/0.010 secs]
  [ParNew 445124K->350518K(773376K), 0.1800791 secs]
* 觀察 CMS-initial-mark 與 CMS-concurrent-reset 之間的 ParNew可發現顯著的下降

  • 其他:Explicit Garbage Collections
Explicit GC指的是用RMI、或是System.gc()、Command Line 一類的方式直接觸發 java application發動GC。這種情況會產如如下的log:
2010-12-16T23:04:39.452-0600: [Full GC (System) [CMS: 418061K->428608K(16384K), 0.2539726 secs]
* 注意那個 (System),即代表此次GC是Explicit GC的

當發生此類事件事,可能需要思考他發生的時機是不是正確的,有沒有必要禁止這類的行為…

與Explicit Garbage Collections有關的Command:
-XX:+ExplicitGCInvokesConcurrent - 需要JDK6+
 - 讓Explicit GC也能Concurrent進行
-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses - 需要JDK6U4+
 - 讓Explicit GC也能Concurrent進行
 - 較建議使用
-XX:+DisableExplicitGC - 禁止 Explicit GC


  • Concurrent Permanent Generation Garbage Collection
FullGC也可能是因為 PermGen的空間不足
範例:
[Full GC  [CMS: 95401K->287072K(1048576K), 0.5317934 secs] 482111K->287072K(5190464K), [CMS Perm : 65534K->58281K(65536K)], 0.5319635 secs]
*此次Full GC 發生時 PermGen的size接近他的上限,由此可知此次FullGC是由PermGen空間不足而觸發的

一般情況,CMS不會對PermGen做GC,可以考慮以下Command:
-XX:+CMSClassUnloadingEnabled加上此選項讓CMS對PermGen做GC
-XX:+CMSPermGenSweepingEnabled在J6U3以前,要額外加上此參數
-XX:CMSInitiatingPermOccupancyFraction=<percent>類似CMSInitiatingOccupancyFraction,當PermGen的使用達到此百分比時,就會對PermGen做GC
-XX:+UseCMSInitiatingOccupancyOnly類似UseCMSInitiatingOccupancyOnly,若沒加上此設定的話,CMSInitiatingPermOccupancyFraction只會作用一次


  • CMS 停止時間的 tuning
CMS會造成 stop the world的情況只有兩個Phase:
 - initial mark phase : 通常不會很久
 - remark phase  : 通常會花較多時間,多執行緒

-XX:ParallelGCThreads=<n> - 可用來指定 remark phase的thread數
 - 若Runtime.availableProcessors() 小於8,則預設值等於 Runtime.availableProcessors()
 - 若 Runtime.availableProcessors() 大於8,則預設值等於 (5/8)*Runtime.availableProcessors()
 - 若系統上有其他的app的話,建議將此數值調低於預設值,否則此application在GC時,會造成其他的application變慢
-XX:+CMSScavengeBeforeRemark強制在 CMS remark之前做一次 minor GC,可減少 CMS remark的工作量,降低所需時間
-XX:+ParallelRefProcEnabled - 使用多執行緒來查找 finalizable object
 - 這邊的多執行緒只針對尋找,並不用來執行 finalizer
 - 適合用在可能有大量 reference與 finalizable object的情況
 - 可以任何GC搭配使用,而不止能用在CMS


  • 結果
如果最終可以得到一個結果是可以滿足latency的需求,那就可以進入下個階段
如果沒辦法滿足需求,那可能會需要降低需求、修改程式,或是將程式佈屬到更多的JVM上