スレッドについて調べる為に「Javaパフォーマンス」を読んだら良いことだらけだった話
きっかけ
普段からよく利用するTomcatにおいて意図したスループットを得るためには、どうやらスレッドについて理解すると良いんじゃないかと考えがこと。
そもそもJavaのスレッドってなんなのか調べようと思い、本書ではスレッドについて言及されていた為、本書を読みました。
- 作者: Scott Oaks,アクロクエストテクノロジー株式会社,寺田佳央,牧野聡
- 出版社/メーカー: オライリージャパン
- 発売日: 2015/04/11
- メディア: 大型本
- この商品を含むブログ (11件) を見る
結局、本書からは当然ながらスレッドに関する明確な答えは得られませんでしたが、アプリケーションの開発/運用において大切なことを学んだ気がするのでまとめたいと思います。
学んだこと
全体を俯瞰する
監視対象の状況を全体的に俯瞰するためにも、まずはテスト環境でJava Flight Recorderを使ってみたいと思います。その便利さを体感できたら、本番でも利用することを進言したいと思います。
意識する指標の明確化
今までのパフォーマンステストを振り返ると、スループット、レスポンスタイムのどちらもごっちゃになったテストをしていたように思います。
今後はこれらの違いを踏まえたテストをやり直そうと思いました。
CPU100%が悪いとは限らない
高負荷状態が長時間続いたら何かの予兆だと感じるのは大切だと思いますが、その瞬間で見ると複数のCPUが全て実行されていることで100%になっている場合もあります。
対象の環境での適切な状態をCPUのコア数を把握したりすることで、実行されるスレッドがいくつになるとCPUの使用率が瞬間的に100%になるのかを予め想定した上で、異常な状態を早期検知できるようにしたいなと思いました。
JITコンパイラの役割
JavaはJavaバイトコードと呼ばれるアセンブリ言語による命令を生成しますが、CPUに依存したバイナリコードはその時点では生成されません。
これがJavaのどの環境でも動作する所以なのですが、実際にJVMが実行する際にはJust In Timeコンパイラによってバイナリコードへと最適化されます。
この最適化の方法にも種類があり、ヒープの使用方法にも関係する為これを知らずにヒープのチューニングはできないと思いました。
JITコンパイラの種類
クライアントコンパイラでは早期に全コードのコンパイルが行われます。一方、サーバコンパイラは実行されるコードのうち、よく利用されるコードを最適化対象としてバイナリコードへとコンパイルします。
よって、ヒープサイズが大きければクライアントコンパイラによって全コードバイナリコードに最適化されますが、小さい場合は途中で最適化が止まってしまうこともあるようです。
ここを知らないとインタプリタ型の言語と同様の実行方法のままになってしまう為、要注意だなと思いました。なお、以下のコードキャッシュがそれに当たります。
起動処理の最適化
起動時間のスピードが求めらる場合は、早期に最適化するクライアントコンパイラを利用すると良いです。
コードキャッシュのチューニング
コンパイルされた命令が格納される領域であるコードキャッシュサイズを調整することもできます。ここの領域がいっぱいになると本来高速に実行されるべきコードが高速に実行されなくなる場合がある為。jconsoleでコードキャッシュサイズを監視する。
ヒープ領域
メモリにはヒープ領域と呼ばれる領域があり、old領域、young領域(eden、survivor)で構成されています。一般的に、オブジェクトは生成されるとedenに話入り当てられます。ガベージコレクションが発生すると利用中のオブジェクトはsurvivor、old領域に移ります。そして、old領域がいっぱいになると未使用のオブジェクトはフルガーベッジコレクションによって破棄されるーという仕組みになっています。
Javaの基本だそうですが、本書を読むまで私は知りませんでした。読み終わってから思ったことですが、これらはJavaアプリケーションが適切にメモリを使っているかを意識、チューニングする為の基礎知識ですね。
ガーベッジコレクタの種類
スループット型ガベージコレクタ、コンカレント型ガベージコレクタがあり、前者はold領域から未使用オブジェクトが破棄される際に、アプリケーションスレッドを停止しますが、後者はフルガーベッジコレクションに近い処理を行う前にコンカレントサイクルというold領域の解放をアプリケーションスレッドと並行して実行することで長時間のアプリケーションスレッドの停止を避けることができるようになっています。
一見すると後者の方が良さそうにも見えますが、old領域の解放だけでコンパクト化は行わないので、young領域からold領域への昇格にold領域の未使用オブジェクトの解放が追いつかない場合はconcurrent ode failureに伴うフルガベージコレクションに近い処理が走りアプリケーションスレッドが停止するので、ガベージコレクタの種類に応じたアルゴリズムを把握しておくことはパフォーマンス劣化の原因分析の為にも大切だと思いました。
ガベージコレクタのチューニング
ヒープサイズの変更
ヒープサイズが小さ過ぎれば、ガベージコレクションが多発しアプリケーションのパフォーマンスは下がります。
ヒープサイズが大きくても、ガベージコレクションの時間は長くなる上、スワッピングが同時に発生するとディスクI/Oも同時に発生し、パフォーマンスは下がります。これがフルガベージコレクションと同発すると自体は最悪になる場合もあります。
この為、適切なガベージコレクタを選択することと、アプリケーションが必要とするヒープサイズを知ることはチューニングの基本となることを知りました。
並列度の指定
ガベージコレクションによって実行されるスレッド数は指定できます。また、ガベージコレクションとアプリケーションのスレッドの実行比率を指定できます。
このことから、マシンの適切な状態をパフォーマンステストで予め把握できるなと思いました。
ガベージコレクタのアルゴリズムの理解
「ガベージコレクタの種類」でも思ったことですが、アプリケーション実行環境のガベージコレクタの種類とその特性を理解した上で、デフォルトの設定によるパフォーマンスが許容範囲に収まっているかを判断するのは、一つのパフォーマンステストに当たると思いました。
実行環境、JVM自体、JVMno設定を把握しておかないと、どのタイミングでフルガベージコレクションが走るかや、その際のCPU使用率がどうなるかーなども適切なパフォーマンステストもできないなーと思いました。
ガーベッジコレクションのログ
ガベージコレクションのログを記録しておくことで、ガベージコレクションの解析を行うことができます。その中でもGC Histgramを活用するとGCによるオーバヘッドをチェックすることができるので便利だと思いました。
また、現状を抑えておけば、取得しておいたログの解析時にガベージコレクションの振る舞いが現状で想定されるものなのかどうかも判断がつくと思いました。
ヒープヒストグラムとヒープダンプ
ヒープヒストグラムではヒープに存在するオブジェクトのヒストグラムを表示してくれます。この為、無駄に生成されているオブジェクトの発見や、ガベージコレクションが頻発する原因の分析に役立てられることを知りました。
また、解析にはその状況を再現する必要がある為、予めヒープダンプを取得しておくと良いということも学びました。
ヒープを非効率に使用するようなアプリケーションになっていないかどうかをチェックして改善するためのテストにも使えるなーと思いました。
ただ、オブジェクトの生成元を辿っていくのは大変みたいですが…
OutOfMemoryError対策
メモリリークやヒープ領域を使い尽くしていることが一つの原因として引き起こされるエラーですが、メモリのサイズが問題なのか、アプリケーションロジックが問題なのかを適切に状況把握する必要があることを知りました。
というのも、適切にメモリを使用するようにプログラミングされたアプリケーションに対して用意されたヒープのサイズが小さいだけといった単純な場合から、コレクションのように自動的に解放されないコレクションが残り続けるようなケースもあり、アプリケーション側で対応しなければならない原因もあるためです。
このようなケースについては、生成するオブジェクトが参照されなくなったらガベージコレクタに解放されるように参照状況を制御する必要があることを知りました。
オブジェクトの参照の仕組みを理解して、アプリケーション開発時のコードレビューするポイントとしたいなと思いました。
Javaアプリケーションの合計メモリ使用量
これはネイティブメモリとヒープの使用量を足したものです。
Javaアプリケーションの実行環境におけるメモリサイズと合計メモリ使用量の関係もスワッピングの発生によるパフォーマンスの低下と関係するため大切です。
まとめ
結局、本書からはスレッドとは何かーという本来知りたいことを調べることはできなかったのですが、Javaアプリケーション/利用するプログラムの動作原理を理解することの大切さについて気づかせてもらえたので、棚から牡丹餅だったと思います。
おかげで、今後こんなことをしていきたいなと思いました。
- パフォーマンスに関する要件定義、作り込み方の見直し
- アプリケーションのコードレビューの見直し
- アプリケーションパフォーマンステストの実施方法の見直し
- アプリケーションサーバ、アプリケーションの監視方法の見直し
また、今回の学習を通して調べたクラスローダーについて、こちらのサイトから以下のようなことも学ぶことができました。
IBM developerWorks 日本語版 : クラスローダーとJ2EEパッケージング戦略を理解する
- JVMが使用するメモリの構造(Permanent領域)
- クラスローダの働き・目的
- Webアプリケーションのクラスローダの仕組み
- JavaEEアプリケーションのパッケージング戦略
- サーブレットが参照するメモリ
後日談 ースレッドについて理解できた話ー
後日、Twitterで「JVMについてもっと知りたいなー」とつぶやいたら、親切な方が以下のドキュメントの所在を教えてくれました。
The Java® Virtual Machine Specification
これを読んだおかげで、次の情報を調べる糸口を掴むことができ、Javaのスレッドとスレッドとは何かーについて理解することもできました( ´ ▽ ` )
JVMのスレッドについて
Threads may be supported by having many hardware processors, by time-slicing a single hardware processor, or by time-slicing many hardware processors.
Threads are represented by the
Thread
class. The only way for a user to create a thread is to create an object of this class; each thread is associated with such an object. A thread will start when thestart()
method is invoked on the correspondingThread
object.
上記から読み取ったこと
- プロセッサがサポートするスレッドを作る為に、スレッド実装者がJavaのThread.classのオブジェクトを生成する。それを起動するにはstart()メソッドを使うーということ
- スレッドはJava固有の概念じゃなくてハードウェアに依存するものであるーということ
プロセッサがサポートするスレッドについて
プロセッサがサポートするスレッドをJavaで制御する様子
こちらの記事を見つけたおかげで、マルチスレッドはプロセッサ、OSがサポートするもので、それを利用する為にJavaプログラマはThread.classを利用してマルチスレッドを実現できるということを理解しました。よかったよかった。
結構な回り道になりましたが、Servletコンテナで意図するパフォーマンスを得るための勉強に今回学んだことを生かしたいと思います。