放課後プログラミング

調べたことや考えたことなどを忘れないために書きます。

ヒープ領域のチューニング

前回の投稿ガベージコレクションの仕組みの続きです。

GCにかかる時間はアプリケーション実行時間の10~15%以下程度に収まるのが良いと言われている。GCにそれ以上の時間がかかっていて、それがボトルネックになっている場合はチューニングを行ったほうが良い。

各種ヒープの設定

JVMの起動オプションに指定することでヒープのサイズ等を設定することができる。
(前回からの引用)

オプション 説明
-Xms? ヒープの初期サイズ、続けて512kのように書いて指定する。 -Xms128m
-Xmx? ヒープの最大サイズ、続けて128mのように書いて指定する。 -Xms512m
-XX:NewRatio=? young世代とtenured世代のサイズ比率、自然数で指定する。NewRatio=Tenured/Young。 -XX:NewRatio=4
-XX:SurvivorRatio=? Eden領域とSurvivor領域のサイズ比率、自然数で指定する。SurvivorRatio=Eden/2Survivor。 -XX:SurvivorRatio=6
-XX:MaxTenuringThreshold=? 殿堂入り閾値自然数で指定する。 -XX:MaxTenuringThreshold=32

チューニングに用いるツール

jdkに標準でチューニングに使えるツールが付属している。
パスは
/Path/To/JDK_install/bin/
僕の場合はwindowsなので
C:\Program Files\Java\jdk1.8.0\bin\
でした。

JavaプロセスのプロセスIDの取得

調べたいアプリケーションのプロセスIDをまずは取得しないと何も始まらない

$ jps
$ jps -lvm

下のオプションをつけるとより詳細に情報が出力される。たとえば僕のIntelliJは以下のようになっていた

$ jps -lvm
4924  -Xms128m -Xmx512m -XX:MaxPermSize=250m -XX:ReservedCodeCacheSize=64m -ea -Dsun.io.useCanonCaches=false -Djava.net.preferIPv4Stack=true -Djsse.enableSNIExtension=false -XX:+UseCodeCacheFlushing -XX:+UseConcMarkSweepGC -XX:SoftRefLRUPolicyMSPerMB=50 -Xbootclasspath/a:C:\Program Files (x86)\JetBrains\IntelliJ IDEA 13.1.1\lib\boot.jar -Didea.paths.selector=IntelliJIdea13
javaプロセスのGCを監視をする
$ jstat -gcutil <Process ID> 1s

$ jstat -gcutil 8416 3s
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
  0.00   0.00  84.07  59.47  91.95  83.64     17    0.036     2    0.070    0.106
  0.00   0.00  94.05  60.17  92.05  84.34     18    0.044     3    0.146    0.190
  0.00   0.00  19.85  65.32  93.08  86.80     20    0.053     4    0.194    0.246
  0.00  46.71  29.49  65.34  93.09  86.03     21    0.057     4    0.194    0.251
  0.00  46.67   0.00  74.73  93.01  86.12     23    0.062     4    0.194    0.256
 99.55   0.00  57.35  74.93  93.31  86.82     24    0.067     4    0.194    0.261
 99.64   0.00   3.00  78.28  93.66  87.19     26    0.096     5    0.256    0.353
 99.64   0.00  12.49  78.28  93.66  87.19     26    0.096     5    0.256    0.353
 99.64   0.00  33.35  78.28  93.66  87.19     26    0.096     5    0.256    0.353

各カラムの意味については

http://docs.oracle.com/javase/jp/6/technotes/tools/share/jstat.html

を参照。見ればわかるがgcutil以外にも様々なオプションがあるので、参照したい対象ごとに適宜使い分ければいい。

Javaプロセスに負荷をかける

apacheJMeterというツールを用いることで簡単にJavaプロセスに負荷をかけられる。

http://jmeter.apache.org/

GUIツール。使い方は割愛。

設定値の影響効果

・Youngを小さくするとSurvivor0で溢れが生じやすくなり、溢れたオブジェクトはTenuredに移るため、FullGCの発生回数が増える。
・Tenuredを小さくするとFullGCの発生回数が増える
・Tenuredを大きくすると1回のFullGCにかかる時間が増える。
・殿堂入り閾値を小さくすると寿命の短いオブジェクトがTenuredに移り、FullGCの発生回数が増える。
・殿堂入り閾値を大きくすると寿命の長いオブジェクトがTenuredになかなか移らず、Survivor0で溢れが生じやすくなり、FullGCの発生回数が増える。

チューニングの方針

jstatで監視しながら、JMeterで負荷をかけてどのようにGCが行われていくかを見てチューニングを行う。一度にたくさんの値を変えてパフォーマンス測定をしても何が原因で良く(悪く)なったのかわからないため、設定値は1つずつ変えていくと良い。ただし複数の値の合わせ技で良く(悪く)もなりうるので、全ての設定値をアプリケーションに"最適"な値に設定するのは非常に難しい。あくまでGCボトルネックになっているときの対応というスタンスで行うのが現実的だと思う。従って、ボトルネック解消のための明確な目標値を設定してからチューニングを行うことが重要。

基本的には前回の投稿に記述したGCの動作が健全に行われているかどうかを各領域の使用率を参照しながら判断し、「設定値の影響効果」を考慮しつつ設定値を調整していく。
たとえばjstat -gcutilで100%に張り付きっぱなしになっている領域があればそれを広げてやればいい。ただし100%に張り付いている原因がその領域の狭さではない場合は他の設定値を適宜変えていく。
あるいはGCを行った際に解放される領域がだんだん狭くなっていくときはメモリリークが起きているので、ヒープ領域のチューニングではなくアプリケーションのバグ修正を行えばいい。

エスケープ解析

メソッド内で生成され、返り値として使われないオブジェクトはヒープではなくスタックに保持することができる。コンパイラはオブジェクトのスコープを解析(エスケープ解析と呼ばれる)し、ヒープに保持するかスタックに保持するかの判断をする。
この機能はJava SE6u23以降に実装されていて、デフォルトで有効になっている。
スタックにGCはなく高速なので、スコープをメソッド内だけにしてそもそもヒープに入れない実装にすることも、GCによるアプリケーションの停止時間を抑えることにつながる。

Permanent領域

ここまでヒープにはYoung世代とTenured世代があり、という話で進めてきたが、クラス定義等のメタデータを格納しておくためのPermanent世代という領域もヒープ内に存在する。TenuredがいっぱいになったときのFullGC時にはこちらも対象となる。
Permanent領域はYoungやTenuredと違い一定の専有サイズを保っていることが多いので、そのサイズが入る大きさにPermanent領域のサイズを設定しておけばだいたい問題ないと思う。

オプション 説明
-XX:PermSize=? Permanent領域の初期サイズ -XX:PermSize=128m
-XX:MaxPermSize=? Permanent領域の最大サイズ -XX:MaxPermSize=128m