JVMのチューニング

前回、JVMとGCのしくみ - ITエンジニアとして生きるでJVMとGCのしくみについて書いた。
今回はその続きということでJVMのチューニングについて書きたいと思う。

JVMチューニングって

  • -Xms ・・・ ヒープ全体(New領域+Old領域)の初期値
  • -Xmx ・・・ ヒープ全体(New領域+Old領域)の最大値

くらいしか話題に上がらないし意識しないことが多い(気がする)。
でもホントはこれだけではダメで、前回のようにPermanent領域、New領域、Old領域を意識したチューニングが必要になる。

VMチューニングを考えるその前に・・・

チューニングの話をする前にまずVMの起動モードについて話したいと思う。
VMには大きく以下2つの起動モードがあり、それぞれ以下のような特徴を持つ。

◆クライアントVMモード

  • 起動時間を短縮し、メモリサイズを縮小するように調整されている。
  • VM起動時に「-client」オプションを付けて実行する。

◆サーバVMモード

  • プログラム実行速度が最大になるように設計されている。
  • VM起動時に「-server」オプションを付けて実行する。

これら2つのモードに違いについてはjvm - Real differences between "java -server" and "java -client"? - Stack Overflowでも取り上げられており、「2つのモードではVMオプションが違う」ということが論じられている。


自環境のJDKがデフォルトでどちらの起動モードになるかを調べるためには「java -version」を実行すれば良い。

以後のチューニングの話は「サーバVM」を前提として話を進める。


Permanent領域のチューニング

Permanent領域のチューニングは以下のシステムプロパティで実施する。
  • -XX:PermSize ・・・ Permanent 領域の初期値
  • -XX:MaxPermSize ・・・ Permanent 領域の最大値

通常はあまり手を入れない設定。
でもTomcat利用している人なら必ず1度は

java.lang.OutOfMemoryError: PermGen space
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClassCond(ClassLoader.java:632)
     :
     :

なエラーに出くわしたことがあるだろう。

TomcatのようなWeb/APサーバは数多くのライブラリをロードするため、デフォルト値(64MB)ではPermanent領域が不足しがちである。
この場合、Permanent領域を拡張する必要があるんだが、経験上「256MB」くらいにしておけばまず問題ないと思う。

-XX:PermSize=256m -XX:MaxPermSize=256m

ちなみにPermSizeのデフォルト値は以下の通り。

  • クライアントVM・・・ 2MB
  • サーバVM・・・ 64MB

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

ヒープ領域のチューニングは以下のシステムプロパティで実施する。
  • -Xms ・・・ ヒープ領域(New領域+Old領域)の初期値
  • -Xmx ・・・ ヒープ領域(New領域+Old領域)の最大値

本来は「OSの空きメモリ量」や「アプリケーションが必要とするメモリ量」を算出して決定すべき値であるが、その算出はなかなか難しい。
実際には論理的に算出するよりも、ある程度決め打ちでメモリを割り当て、負荷試験を実施しながらメモリ量に問題がないことを確認していくケースが多いと思う。
「OSの空きメモリ量」が許すのであれば、とりあえず「2GB」としておけば良い。(※)

-Xms2048m -Xmx2048m

※ JVM Max Heap Size recommended (Performance forum at Coderanch)で論じられているように、32bitのHotspotJVMでは最大ヒープサイズは「2GB」とされているため、「2GB」以上のヒープを割当てる場合は注意が必要。


New領域とOld領域のチューニング

New領域とOld領域のチューニングは以下のシステムプロパティで実施する。
  • -XX:NewRatio ・・・ New 領域と Old 領域の比率
  • -XX:NewSize(-Xmnでも可) ・・・ New 領域の初期値
  • -XX:MaxNewSize ・・・ New 領域の最大値
  • -XX:SurvivorRatio ・・・ Eden領域とSurvivor領域の比率

まずはNewRatioについて。
NewRatioのデフォルト値は以下の通り。

  • クライアントVM・・・ 8
  • サーバVM・・・ 2

つまり、サーバVMのデフォルトでは「New領域:Old領域=1:2」となっており、ヒープ全体の1/3がNew領域、2/3がOld領域に割り当てられることになる。
通常、New領域のサイズは最大ヒープサイズ(-Xmx)の1/4〜1/2で設定するのがベストプラクティスとされているため、デフォルト値でOK。
きめ細やかなチューニングを必要とする場合は見直す場合もあるだろうが、負荷試験等で問題が発生しない限り特に変更する必要は無いだろう。


次に-XX:NewSize(-Xmn)と-XX:MaxNewSizeについて。
これは上記説明の通りNew領域のサイズをしているするもの。
ただこの辺の設定は「-Xms」「-Xmx」「-XX:NewRatio」の3つで指定する方が分かりやすいと思うので私はあまり利用しない。
前述の通りNew領域のチューニングにおける関心事は「最大ヒープサイズ(-Xmx)の何分の1とするか?」であるため、「-XX:NewRatio」にて設定をする方が良いと思う。


最後に-XX:SurvivorRatioについて。
SurvivorRatioのデフォルト値は以下の通り。

  • クライアントVM・・・ 8(だっけ?)
  • サーバVM・・・ 8

つまり、サーバVMのデフォルトでは「Eden領域:From領域:To領域=8:1:1」となっており、New領域の8割がEden領域に割り当てられていることになる。
この値は本来「どの程度の頻度でオブジェクトがインスタンス化されるか?」を元に算出して決定すべきであるが、その算出は難しい。(ひとつひとつのオブジェクトがどの程度メモリを喰うかを算出するのは難しいため)
なので論理的に算出するのではなく、ひとまずデフォルト値(8)にしておいて、負荷試験を実施しながら問題あるかどうかを確認していく方針とするのが良いだろう。
ちなみに私の経験上ではデフォルト値でも問題ないことが多かった。

ということで、New領域とOld領域のチューニングは

-XX:NewRatio=2 -XX:SurvivorRatio=8

とするのが良いだろう。

初期値と最大値

チューニングに関するシステムプロパティには「初期値」と「最大値」を設定するものがある。(「-Xms ⇔ -Xmx」、「-XX:PermSize ⇔ -XX:MaxPermSize」)
これは基本的に同値に設定しておくべきである。

「初期値 ≠ 最大値」とした場合、初期化時には最大アドレス空間が「仮想的」に確保され、必要になるまで物理メモリは割り当てられない。(初期値で処理がまかなえなくなった場合に、この「仮想的」な部分に物理メモリが割当てられる。)
この物理メモリを割当てている瞬間はユーザビリティを落とすことがあるかもしれない(※)し、わざわざこのような無駄な処理を引き起こす設定としない方が良いと考える。
そのため、ベストプラクティスとして「初期値=最大値」とすることを推奨したい。

※ 実際にそういったことに出くわしていない(「初期値≠最大値」で運用したことがない)ので、あくまでここは可能性の話ということで。


さいごに

ここまでの話を統合すると、こんなパラメータになる。
-XX:PermSize=256m -XX:MaxPermSize=256m -Xms2048m -Xmx2048m -XX:NewRatio=2 -XX:SurvivorRatio=8


今回はPermanent領域、New領域、Old領域を意識したチューニングをすべし、ということを話した。
これは“Stop the World”を防ぐコンカレントGCとは? (1/2):現場から学ぶWebアプリ開発のトラブルハック(2) - @ITで紹介されているコンカレントGCを利用する際には特に大事になってくる。
(コンカレントGCを利用すれば前回述べていたようなFull GC時の懸念点が解消されるが、うまくチューニング(特にNew領域)しないと逆に性能を落とすことになる。)



いつかコンカレントGCについても記事を書きたいな〜。