O'Reilly Japan - Javaパフォーマンス
こちらの本の3章まとめ
1章 イントロダクション - Qiita
2章 パフォーマンステストのアプローチ - Qiita←前回記事
3章 Javaパフォーマンスのツールボックス - Qiita←今回記事
4章 JIT コンパイラのしくみ - Qiita ←次回記事
5章 ガベージコレクションの基礎 - Qiita
3.1 オペレーティングシステム付属のツールと分析
手始めに使うツールはJavaとは関係のないツール。
Unixベースのシステムでは、sarコマンド、vmstat、iostat、prstatがあるし、
Windowsだとtypeperfというものがある。
CPUの使用率
CPU使用率は以下2つに分類できる。
- ユーザー時間: CPUがアプリケーションを実行している時間
- システム時間: CPUがOSのカーネルを実行している時間
システム時間はアプリケーションと無関係ではない。ディスクへ書き込みを行ったり、ネットワークへ転送したりなどといったことをしています。
※Windowsではシステム時間は特権時間(privileged time)と呼ばれる
CPUの使用率をできるだけ高くし、実行時間を短くすることを目標とする。
CPUがアイドル状態の原因は
- 同期プリミティブでロックの開放待ち
- 何かを(例えば、データベースからのレスポンス)を待っている。
- 行うべき処理が無い。
といったことが考えられる。
逆に1プログラムあたりのCPU使用率を制限したい場合もある。
そういう場合は、一定の間CPUサイクルを強制的にアイドル状態にしたり、優先順位を下げたりといったことができる。
CPU のランキュー
WindowsとUnixのシステムはともに、実行可能な(入出力待ちやスリープ中でない)スレッドを監視する仕組みがある。
この仕組みをランキュー(run queue)と呼ぶ。
Windowsではプロセッサキューと呼ばれ、typeperfで見ることができる。
C:> typeperf -si 1 "\System\Processor Queue Length"
"05/11/2013 19:09:42.678","0.000000"
"05/11/2013 19:09:43.678","0.000000"
ランキューは現在実行中のものが含まれるため、かならず 1以上 となる。
一方で、Windowsのプロセッサキューは現在実行中のものが含まれないため 0以上 となる。
CPUの数よりも、多い数のスレッドを実行する場合は、パフォーマンスが低下する。
Unixでは、ランキューの数 == CPUの数となるようにし、Windowsではプロセッサキューの長さが0となることが望ましい。
ただし、これは絶対の原則ではない。
ランキューの長さが時々増加する程度であれば、問題はない。
一方で、ランキューが長過ぎる状態が継続する場合、マシンの負荷が高くなる。処理を別のマシンへ分散させるか、コードを最適化する必要がある。
ディスクの使用率
問題が起こっていることを確認するヒントとなる。
例えば、iostat -x
のwMB/s(秒間書き込みbyte数)が低いのにw/s(秒間の書き込み回数)が多い場合は、書き込み処理をまとめたほうがパフォーマンスが上がるかもしれません。
逆にまた、書き込みが多すぎる場合は、そこがボトルネックになっていることが分かる。
システムがスワッピングを行っているかどうかわかる。パフォーマンスに影響を及ぼす可能性がある。
vmstatでは、si(スワップイン)とso(スワップアウト)が見れる。
ネットワークの使用率
ネットワークトラフィックの監視は標準のシステムツールは能力不足。
Unix系システムではnetstatが使われる。Windowsではtypeperf。
ネットワークの帯域幅は、Unix系システムではnicstatが使われる。
3.2 Java の監視ツール
- jcmd: 指定したプロセスについての情報が出てくる
- jconsole: GUIでJVMの動きを見ることができる。
- jhat: Webブラウザでヒープダンプをブラウズできる。
- jmap: ヒープダンプ、メモリの情報を取得できる。
- jinfo: JVMのシステムプロパティを表示できる
- jstack: Javaプロセスのスタックをダンプする。
- jstat: GCやクラスローディングについての情報を表示。
- jvisualvm: JVMの監視。ヒープダンプの解析ができる。
JVMの基本的な情報
実行時間
jcmd ${プロセスID} VM.uptime
システムプロパティ
System.getProperties()
した値(起動時に-Dhogehoge
と指定するやつ)と同じ値が見れる。
jcmd ${プロセスID} VM.system_properties
jinfo -sysprops ${プロセスID}
JVM のバージョン
jcmd ${プロセスID} VM.version
JVM のコマンドライン引数
jcmd ${プロセスID} VM.command_line
JVM のチューニングフラグ
jcmd ${プロセスID} VM.flags -all
-allをはずすと、指定したものだけになるのかな?
プラットフォーム固有のデフォルト値を表示
java -XX:+PrintFlagsFinal -version
:=となっているものはデフォルト以外の値が指定されている。
フラグの動的変更
まず、↓で設定されている値全てを見ることができる。
jinfo -flags ${プロセスID}
設定項目毎に見たい場合は
jinfo -flag PrintGCDetails ${プロセスID}
設定値を変更するときは
jinfo -flag -PrintGCDetails ${プロセスID}
jinfo -flag PrintGCDetails ${プロセスID}
スレッドの情報
jconsoleやjvisualvmでGUIで実行しているスレッド数を確認できる。
スレッドスタック表示の方法
jstack ${プロセスID}
jcmd ${プロセスID} Thread.print
クラスの情報
jconsoleやjstatを使う。
jstatではコンパイルに関する情報も得られる。
ガベージコレクションの動的な分析
jconsoleではヒープの使用率をグラフで表示できる。
jcmdではGCを実行できる(jconsoleでもできたような?)
jmapはヒープの概要が見れる。
jstatでは、GCが行っていることを表すさまざまなビューが用意されている。
ヒープダンプの事後的な分析
jvisualvm(GUIツール)かjcmdやjmapで取得可能
標準ツールだとjvisualvmやjhatで解析可能
Eclipse Memory Analyzer Toolというサードパーティー製のツールもある。
3.3 プロファイリングツール
サンプリング型のプロファイラ
サンプリングモードとinstrumentedモードがある
サンプリングモードはオーバーヘッドが最小のため、プロファイラの介入によるパフォーマンス特性をできるだけ変化させないようにすることができる。
しかし、サンプリング型のプロファイラは正確ではないものが多い。
例えば、タイマーで定期的に呼び出され、タイマー発生時に実行されたスレッドしか感知しない事象など。
ほとんどの場合、プロファイリング結果の先頭に現れるメソッドは2~3%を占めているのみなので、がんばってこれを2倍早くしたとして1%の高速化にしかならない場合もある。
instrumented 型のプロファイラ
サンプリング型とは違って、それぞれのメソッドが呼び出された回数や、1秒あたりに呼び出される平均回数なども見ることができる。
これによって例えば、実装を高速化すべきか、実行回数を抑えるように改善すべきかなどが分かる。
instrumentedモードのプロファイラは、呼び出し回数をカウントするコードなどをバイトコードを書き換えて仕込んでいるため、パフォーマンス面で不正確な場合がある。
例えば、メソッドのコードのサイズが増えて、インライン化が不要と判定されてしまう(4章で詳しく説明される)可能性がある。
サンプリング型プロファイラはsafepoint(メモリを割り当てられている状態)にあるスレッドしか、プロファイリングの対象にならないが、instrumentedモードのプロファイラだと対象となる。
ブロックされたメソッドとスレッドのタイムライン
ブロックするメソッド(LockSupport#parkやObject#wait()など待機するメソッド)は、CPU時間を消費しない(CPU使用率上昇しない)ため、プロファイリング結果の上位に出てきていたとしても、最適化はできない。
そのため、ほとんどのプロファイラはブロックされているメソッドはデフォルトでは表示されない。
ブロックしているメソッドについては、スレッドの実行状況を見たほうが動きが分かる。VisualVMであれば、Threadsタブ。
ネイティブなプロファイラ
ネイティブなプロファイラを使えば、JVMそのもののプロファイリングができる。
ネイティブなプロファイラにOracle Solaris Studioというものがある。
Solarisという名前が付けられているが、Linuxでも動作する。
Solaris上で実行した場合には、Solarisカーネルの内部構造を活用してより多くの情報を取得できます。
というふうには記載があった。Solarisマシンを持ってないので、試せないですが。
ネイティブなツールでしか取得できないデータとして、GCに費やした時間などが挙げられる。
3.4 Java Mission Control
商用版のJavaにjmcという名前で入っているもので、オープンソース版のものには入っていない。利用するには商用ライセンスが必要。
JFR
JFR(Java Flight Recorder)という機能がjmcのキーとなる機能。
スレッドがブロックしているなど、イベントが分かる。
JFRのMemoryビューではガベージコレクションに関するイベントが見れる。
5章や6章では、このツールがどう役に立つかを意識しながら読むといいとのこと。
JFRのCodeビューのOverviewタブではパッケージ毎に集計された値が見れる。この機能があるのは珍しい。
ロックインフレーションに関する情報も正確に取得できる。
その他にもjcmdやjconsoleでは取得できない情報が取得できる。
※9章でも解説されるが、ロック取得するには、単純なループのなかで待機(スピンと言う)し、ロックインフレーションと呼ばれるプロセスのなかでCPUやOSに固有のコードを使ってロックの獲得をする。
JFRの有効化
有効化するには、アプリケーションを起動するコマンドラインで
-XX:+UnlockCommercialFeatures
-XX:+FlightRecorder
というフラグを付与する。
JFRは2つの記録方法を取れる。
一定期間か連続記録があり、連続記録ではリングバッファーが使われる。
-XX:+FlightRecorderOptions=${パラメーター文字列}
といったパラメータでどのように記録するかを指定できる。
オプションはここに記載がある。
実行中のJVMに対しても以下コマンドで設定が可能(-XX:+FlightRecorder
オプションはあらかじめ指定しておく必要があるが)。
jcmd ${プロセスID} JFR.start [ オプション ]
継続的な記録の場合は、以下コマンドでリングバッファ内のデータをファイルへ出力できる。
jcmd ${プロセスID} JFR.dump [ オプション ]
実行中の記録に関する情報を以下コマンドで出力できる(ちなみに、1プロセスに複数の記録もできるらしいので、複数の記録をしている場合に使うことが多いのかなあ)。
jcmd ${プロセスID} JFR.check [verbose]
↓で、記録を中止する。
jcmd ${プロセスID} JFR.stop [ オプション ]
JFR のイベントの選択
JFRは拡張することができる。独自のイベントを作れるらしい。
イベントの収集には、必然的にオーバーヘッドがかかる。
しかし、オーバーヘッドがあっても取得したいイベントもある。
例えば、TLAB(Thread Local Area Buffer)のイベントを監視し、オブジェクトがold領域に直接割り当てられたかどうかなど。