O'Reilly Japan - Javaパフォーマンス
こちらの本の5章まとめ
1章 イントロダクション - Qiita
2章 パフォーマンステストのアプローチ - Qiita
3章 Javaパフォーマンスのツールボックス - Qiita
4章 JIT コンパイラのしくみ - Qiita ←前回記事
5.1 ガベージコレクションの概要
Javaの魅力的な点は開発者がオブジェクトのライフサイクルを意識しなくて良いところ。
しかし、メモリ利用の最適化をする場合には、これは弱点となるかもしれない。
この本の筆者の経験によると、他言語でダングリングポインタやヌルポインタにまつわるバグを潰している時間よりもJavaのメモリに取り組んだ時間のほうが短いとのこと。
どこからも参照されていない場合、GCの対象となる。
しかし、連結リストなど、要素が次の要素を参照している構造のリストがあったとして、そのリスト自体参照をしていないのであれば、リスト全体が開放可能。
使われていないオブジェクトを定期的にヒープ内で探索している。
メモリの断片化を防ぐために、どこかで、メモリ領域を結合する必要がある。
ガベージコレクションのパフォーマンスは、以下3つで決まる。
- 未使用のオブジェクトの発見
- メモリの解放
- ヒープのコンパクト化
オブジェクトの参照を調べるときや、オブジェクトのメモリ上の位置を移動するときはアプリケーションスレッドがこれらのオブジェクトを利用していない状態にする必要がある。
アプリケーションスレッドが止まることをstop-the-world pauseと呼ぶ。
世代に基づくガベージコレクター
ヒープ領域は以下領域に分けられる。
- old領域(tenuredとも)
- young領域。さらに以下2つに分けられる
- eden(楽園)
- survivor(生存者)
なぜ、複数に分けられているかというと、ほとんどのオブジェクトは短い期間しか使われないため。
オブジェクトはまず、young領域内のedenに割り当てられる。
マイナーガベージコレクションは、この領域がいっぱいになると、ガベージコレクターはアプリケーションスレッドをすべて止めて、使われているオブジェクトは別の領域へ移動し、young領域を空になる(使われていないオブジェクトはそのまま消える)。
このやり方のメリットは以下2つ
- ヒープ領域の一部であるyoung領域だけなので、高速。※young領域が小さい場合、頻繁にマイナーGCが走る。
- eden空間かold領域のいずれかに移動される。すべてのオブジェクトがなくなるため、コンパクト化の必要が無い。
old領域への移動を繰り返していると、old領域もいっぱいになる。
old領域の処理はガベージコレクションのアルゴリズムによって異なる。
処理は複雑になるが、CMSとG1はアプリケーションスレッドを止めずに不使用のオブジェクトを探すことが可能。
しかし、消費するCPUサイクルは増える。
これらのガベージコレクションもフルガベージコレクションを行うことがある。
コンカレント(並行)型ガベージコレクションのチューニングでは、フルガベージコレクションを避けることが大きな目標の一つ。
どのガベージコレクターを採用するかの指針
レスポンスタイムが重要な場合、
スレッドの停止(特にフルガベージコレクション)で一部リクエストへの影響があるが、それを抑えたいのであれば、コンカレント型ガベージコレクション
レスポンスタイムの平均値が重要であれば、スループット型ガベージコレクター
バッチ処理型であれば、
CPUリソースに余裕があればコンカレント型ガベージコレクション。フルガベージコレクションによる停止を回避できて、処理を速く終わらせられる。
CPUリソースが限られている場合は、余計に遅くなる。
ガベージコレクションのアルゴリズム
4つある
1. シリアル型ガベージコレクター
もっともシンプル。
ヒープの処理を行うスレッドは1つ。
マイナーガベージコレクションとフルガベージコレクションの両方でアプリケーションスレッドを停止して行う。
-XX:+UseSerialGC
で有効にできる。
2. スループット型ガベージコレクター
young領域の処理に複数のスレッドを使うので、マイナーガベージコレクションはシリアル型よりも高速。
有効化するためには、
-XX:+UseParallelGC
-XX:+UseParallelOldGC
(old領域の処理にも複数のスレッドを使う)
とする。
3. CMSガベージコレクター
フルガベージコレクションに伴う長い停止を回避するために、CMS(Concurrent Mark Sweep)が設計された。
アプリケーションスレッドはマイナーガベージコレクションのときにしか停止しない。
代わりにCPUリソースを消費する。バックグラウンドのスレッドは、コンパクト化を行わないため、ヒープが断片化したままになる。
CPUリソースが足りない場合や、ヒープの断片化が進行してオブジェクトの割り当てが不可能になった場合は、シリアル型と同じ動作を行う。
有効化するには、
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
とする。
4. G1ガベージコレクター
G1(Garbage 1st)は、大きなヒープ(4GB以上)を最低限の停止時間で処理することを目標としている。
ヒープを複数のリージョンに分けて処理を行っている。
1つのリージョン内で使われているオブジェクトを別のリージョンにコピーする。そうすると、自動的に(部分的には)コンパクト化されているという理屈。
CMSと同様にフルガベージコレクションを避けているので、CPUリソースは多く消費される。
有効化するには、
-XX:+UseG1GC
とする。
ガベージコレクションの強制的な実行と無効化
マイナーガベージコレクションはyoung領域がいっぱになると発生。
フルガベージコレクションはold領域がいっぱいになると発生。
コンカレント型ガベージコレクションはヒープがいっぱいになりそうな時点で発生する。
System.gc()
でガベージコレクションを強制的に発生させてもほとんどいいことはない。
CMSやG1のガベージコレクションが使われていても必ずフルガベージコレクションが行われる。
いずれ発生するガベージコレクションを前倒して行うだけで、パフォーマンス向上にはならない。
ただし、パフォーマンスの監視やベンチマークの測定の場合には意味がある。
測定の前にガベージコレクションを行い、クリーンな状態で測定を行うことができる。
ヒープダンプの出力の前にガベージコレクションを行うと、ヒープの分析がしやすいメリットもある(ただし、ほとんどの場合、ヒープダンプ取得する手法のほとんどは自動的にガベージコレクションが行われる)。
jcmd ${プロセスID} GC.run
でガベージコレクションを発生させられる。jconsoleでもできる。
RMI(遠隔メソッド呼び出し)で、分散ガベージコレクターのしくみとしてSystem.gc()
が1時間ごとに実行される。
System.gc()
の呼び出しを無効化したい場合は、-XX:+DisableExplicitGC
とすればできる。
ガベージコレクションのアルゴリズムの選択
シリアル型ガベージコレクターが有効なのは、利用するメモリが100Mbyte以下の場合に限られる。
ほとんどのプログラムでは、スループット型かコンカレント型が選択される。
2章で説明した、パフォーマンス上の目標に応じてどれを使うか決定してください。
アプリケーションの実行時間、スループット、平均値(または90パーセンタイル値)のどれを優先するのかといったことを考慮して決定してください。
すべてのCPUリソースを消費するほどではないバッチ処理では、コンカレント型ガベージコレクションが向いている。
すべてのCPUリソースを消費するバッチ処理は、スループット型ガベージコレクションが向いている。
CPUリソースに制限があり、CMSのバックグラウンドのスレッドを実行する余裕が無くなり、CMSのパフォーマンスが大幅に悪化することがある。
この状態を、concurrent mode failure(コンカレントモードの失敗)と呼ぶ。
レスポンスタイムの平均値では、スループット型ガベージコレクションのほうが高く、
90パーセンタイル値では、CMSとのほうが高い場合がある。
基本的には、ヒープサイズが4Gbyte以下の場合はG1よりもCMSのほうが高速。
CMSのバックグラウンドのスレッドはold領域全体をスキャンしてから、オブジェクトの開放を行う。スキャンする時間は領域のサイズに比例する。
スキャンとオブジェクトの開放より先にヒープがいっぱいになると、concurrent mode failureが発生する。
発生したら、アプリケーションスレッドを全部止めてフルガベージコレクションを行う。
フルガベージコレクションで使われるスレッドは1つだけのため、パフォーマンスは悪化する。
ここで複数スレッドを使わせるチューニングも可能だが、その分だけ、各スレッドの処理も増加する。
concurrent mode failureが発生する確率は割り当てメモリの量にも影響する。
これに対してG1ガベージコレクターでは、old領域がリージョンで分割されているため、old領域のスキャンをリージョン毎に別々のスレッドで処理することも可能。
これらのスレッドの処理が追いつかなくなった場合でも、concurrent mode failureが発生するはあるが、仕組み上、あまり発生しない。
CMSでは、フルガベージコレクション以外ではコンパクト化しないため、断片化しやすい。
G1ではヒープの断片化が発生しづらい。
concurrent mode failure を避けるためのチューニング方法は、CMSにもG1にもある。
5.2 ガベージコレクターの基本的なチューニング
ヒープサイズの変更
ヒープが小さいと、なんどもガベージコレクションを行わないといけなくなる。
ガベージコレクションの際に発生する一時停止の時間は、ヒープサイズに比例して長くなる。
物理メモリ以上のヒープ領域を指定すると…
JVMはスワップ領域であるかどうかを区別していない。なので、Javaアプリケーションは指定したヒープ領域をフルに使おうとするので、パフォーマンスに影響が出る。
さらに、フルガベージコレクションの際には、ヒープ全体へのアクセスがされるため、かならずスワッピングも発生する。
そして、ガベージコレクションが間に合わず、concurrent mode failureが発生することにつながる。
なので、 ヒープのサイズの指定は物理メモリの量を超えてはいけない。
物理メモリは、JVM自身や他のアプリケーションのために、1Gbyteの余裕を持っておいたほうがよい。
ヒープサイズの指定は、初期値-XmsN
と最大値-XmxN
がある。
デフォルト値はOSやメモリ容量、JVMの種類で異なる。
初期値と最大値の間でJVMは自動的にチューニングを行う。現状のヒープサイズのままではガベージコレクションが発生しすぎるといった場合には、ヒープサイズを継続的に拡大する。
一般論としては、フルガベージコレクションの後に30%使われている程度のヒープサイズが望ましいとのこと。
各領域のサイズ設定
-
-XX:NewRatio=N
: young領域に対するold領域の比率(デフォルトは2) -
-XX:NewSize=N
: young領域の初期サイズ -
-XX:MaxNewSize=N
: young領域の最大サイズ -
-XmnN
: NewSizeとMaxNewSizeを同じ値にする短縮記法
young領域の初期サイズは以下計算で求められる。
young領域の初期サイズ = ヒープの初期サイズ/(1 + NewRatio)
デフォルトではyoung領域の初期サイズはヒープの初期サイズの33%になる(NewSize
フラグで指定することもできる)。
permanent 領域とメタスペースのサイズ変更
JVMはクラスに関するデータを保持します。その領域がJava7まではPermanent領域と呼ばれ、Java8以降はメタスペースと呼ばれる。
Java7のPermanent領域は、クラスのデータとは関係のない、雑多な情報も入っていたが、Java8からは通常のヒープに移された。
permanent領域やメタスペースは、JITコンパイラやJVMが利用する領域なので、ほとんど意識はしない。
permanent領域は上限があるが、メタスペースはデフォルトでは上限がない(上限サイズを指定する必要もほとんどない)が、
一応載せておくと
Permanent領域の設定: -XX:PermSize=N
, -XX:MaxPermSize=N
メタスペースの設定: -XX:MetaspaceSize=N
, -XX:MaxMetaspaceSize=N
メタスペースでメモリを消費し尽くしてしまう場合があるが、それは8章で紹介されるNMT(Native Memory Tracking)で分析できるとのこと。
(システムが巨大になりすぎて、クラスが増えるとそうなるのかな?そうなったらというかそうなるまえにサービス分割を考えたほうが良さそう)
permanent領域やメタスペースのサイズ変更は、ガベージコレクションを伴うため、遅い。
起動時にガベージコレクションを繰り返していたら、permanent領域やメタスペースの拡大がされている可能性がある。
そういう場合は初期サイズを大きくする。
7章で解説するヒープダンプを使うと、クラスローダーの情報を取得できる。クラスローダーからのデータでpermanent領域やメタスペースがいっぱいになっていないかが分かる。
-clstats
を指定して(Java7の場合は-permstat
)、jmapを起動するとクラスローダーに関する情報が取得できる。
並列度の指定
シリアル型以外では、マルチスレッドでガベージコレクションが行われている。
-XX:ParallelGCThreads=N
でスレッド数を指定できる。
デフォルトではCPU1個につき1スレッドとなるが、8を超えた場合、以下計算式でスレッド数が決まる。
ガベージコレクションのスレッド数 = 8 + 5(N - 8)/8
複数のJVMが起動している場合は、スレッド数を減らしたほうがよい。
ガベージコレクションの効率は高いので、CPU100%使用する。
adaptive sizing
ヒープ領域、survivor空間のサイズは実行中に動的に変えていく。この動きをadaptive sizing(適応型のサイズ変更)と言う。
最大値に大きな値を設定しても使わないのにヒープを過剰に使用するかもしれないという状況にならずに、自動的に拡大されるというのがメリット。
サイズ変更には時間がかかる。その大部分がガベージコレクションによる停止。
ガベージコレクションのパラメーターを細かく指定し、必要なヒープサイズも分かっているのであれば、adaptive sizingを無効にしてもOK。
-XX:-UseAdaptiveSizePolicy
で無効にできる。
-XX:+PrintAdaptiveSizePolicy
を使うと、それぞれの空間が拡大される様子がわかる。
ガベージコレクション関連のツール
ガベージコレクションがアプリケーションにどれだけ影響しているかはガベージコレクションのログを見るのがよい。
-verbose:gc
か-XX:+PrintGC
でGCのログが出るようになる。
-XX:+PrintGCDetails
でもっと詳細なログが出る。
また、合わせて、-XX:+PrintGCTimeStamps
か-XX:+PrintGCDateStamps
を指定するようにする。
-Xloggc: ${ファイル名}
で出力先を変更できる。
ログローテーションに関するフラグは以下。
-XX:+UseGCLogFileRotation
, -XX:NumberOfGCLogFiles=N
, -XX:GCLogFileSize=N
GC Histogramというツールにログファイルを読み込ませれば、グラフや表を生成できる。
jstat -gcutil ${プロセスID} 1000
で起動しているJavaアプリケーションに対してGCログが取得できる。