“Stop the World”を防ぐコンカレントGCとは?:現場から学ぶWebアプリ開発のトラブルハック(2)(1/2 ページ)
本連載は、現場でのエンジニアの経験から得られた、APサーバをベースとしたWebアプリ開発における注意点やノウハウについて解説するハック集である。現在起きているトラブルの解決や、今後の開発の参考として大いに活用していただきたい。(編集部)
Full GC問題の時代が再び到来!
それまで順調に動いていたはずのWebアプリケーションが、ある時突然、応答を返さなくなる。そして、運用者があたふたしている間に、何事もなかったかのように再び動き出す。
Javaで構築したシステムにかかわる者ならば誰しもが体験するであろう事象、そうFull GC(ガベージ・コレクション)だ。Full GCが行われている間、すべてのアプリケーションスレッドは停止する。この事象は“Stop the World”とも呼ばれている。
Full GCに対しては、世代別GCを適切にチューニングし、発生回数を減らす事で対処してきた。しかし、世代別GCのチューニングでは、Full GC1回当たりの停止時間を短くできない。サーバに搭載されるメモリが数Gbytesを超えるのが当たり前になってきた現在では、Javaに指定するHeapサイズも1Gbyteを超えるケースが出てきている。そのためHeapサイズの巨大化に伴い、Full GC時間が無視できなくなってきている。
本稿では、Full GC時間を改善するための手段となる「コンカレントGC」について解説する。
ガベージ・コレクションにもいろいろある
■従来のGC=世代別GCとは?
コンカレントGCの解説の前に、従来の世代別GCについて簡単に振り返っておこう。世代別GCは、Java VMのHeap領域をNew世代/Old世代に分け、オブジェクトの生存期間に応じてGCを効率化する。生成された後、すぐに不要となる短命オブジェクトをNew世代領域で回収し、比較的長い期間必要となる長命オブジェクトをOld世代領域で長期的に管理する。
例えば、Webアプリケーションでは、以下のようにオブジェクトを分類できるだろう。
- 短命オブジェクト
- レスポンス返却後は不要となるオブジェクト。例えば、リクエストパラメータとなるStringオブジェクトや、ServletRequestに格納されたオブジェクト
- 長命オブジェクト
- HttpSessionに格納されたオブジェクト
- ServletContextやClassのstatic領域などに格納されたキャッシュ的なオブジェクト
■Old世代のGC(メジャーGC=Full GC)とNew世代のGC(マイナーGC)
New世代のGC(マイナーGC)には、高速なCopy方式のGCが使用される。一方、Full GCとも呼ばれるOld世代のGC(メジャーGC)にはMark-Sweep-Compact方式のGCが使用される。Mark-Sweep-Compact方式のGCは、その名のとおり、以下の流れで処理を行う。
- 生存中のオブジェクトをすべてマーキング(Mark)
- マークが付いていないオブジェクトを除去(Sweep)
- 空き領域を確保するためのオブジェクトのHeap内再配置を行う(Compact)
メジャーGCはコストが高く、時間がかかってしまう。
これらのGCは、高速化のために複数のスレッドで実行することもでき、それらはパラレルGCと呼ばれている。しかし、いくら複数のスレッドでGCを実行しても、その間すべてのアプリケーションのスレッドは停止される。
編集部注:世代別GCについて詳しく知りたい読者は、連載:事例に学ぶWebシステム開発のワンポイントの第6回「APサーバからの応答がなくなった、なぜ? ―GCをチューニングしよう―」や、連載:チューニングのためのJavaVM講座の後編「ガベージコレクタの仕組みを理解する」をご参照ください。
本稿ではコンカレントGCと区別するために、従来の世代別GCを「スループットGC」と呼ぶ。
■コンカレントGCとは?
コンカレントGCはJ2SE 1.4から使用できるGC方式で、Old世代のGCをアプリケーションスレッドと並列に実行する。
しかし、GCのアルゴリズム上、すべてのアプリケーションスレッドを停止する必要のある期間がどうしても存在する。コンカレントGCでは、GCを次の4つのフェイズに分割して処理することで、その期間を最小化している。
- Initail Mark
- 生存オブジェクトをたどるための出発点(root)となるオブジェクトをマークする。rootとなるオブジェクトは限られているため、マーキングはすぐに終了
- すべてのアプリケーションスレッドを停止させる必要がある
- Concurrent Mark
アプリケーションスレッドと並列に、rootからたどれるオブジェクトに対してマーキングを進める。並列に実行しているため以下のような状況が発生- マークしたオブジェクトに新たにオブジェクトの参照が追加され、生存しているのにマークされていないオブジェクトが存在
- マークしたオブジェクトが使われなくなり、回収漏れとなるオブジェクトが存在
- Remark
- Concurrent Markフェイズで実施したマーキングの整合性を確保する
- 新たに追加されたオブジェクトの参照をチェックし、マーク漏れをなくす
- マークしたオブジェクトが不要になっていたとしても、次回のGCで回収されることを期待し、何も行わない
- すべてのアプリケーションスレッドを停止させる必要がある
- Concurrent Markフェイズで実施したマーキングの整合性を確保する
- Concurrent Sweep
- マーキングされていないオブジェクトは参照がなくなったと判断されるため、それらを回収し空き領域を作成
- アプリケーションスレッドと並列に行われる
このような仕組みにより、コンカレントGCでは“Stop the World”を回避している。
コンカレントGCの問題点
一見良いことずくめのように思えるコンカレントGCだが、問題点もある。コンカレントGC実行中はGCスレッドが動作している分、負荷が高くなる。そのため、全体的なスループット(ある単位時間当たりの処理能力)は下がり、応答時間も遅くなるのだ。
どれくらい性能が劣化するかについては、アプリケーションの作り方や、システムに対する負荷状況により異なる。筆者の経験では、コンカレントGCの高負荷時の性能がスループットGCと比較し、30%程度低下したこともあった。このような大幅な性能劣化があると、コンカレントGCの採用を見送らざるを得ない。
■性能劣化の原因
コンカレントGCはなぜここまで性能が劣化してしまうのだろうか。実はコンカレントGCでは、New世代領域に関連するオプションのデフォルト値として「-XX:SurvivorRatio=1024 -XX:MaxTenuringThreshold=0」という値が設定される(オプションの意味については、後述)。そのため、1回のマイナーGCで回収されなかった短命オブジェクトはすぐにOld世代領域に移動してしまう。結果として、Old世代領域の使用量が上がりやすくなり、メジャーGCの回数が増えてしまうのだ。
この問題を解決するための手段として、コンカレントGCと世代別GCを組み合わせて使用することが可能だ。以降では、マイナーGCを活用したコンカレントGCのためのチューニングについて見ていこう。
Copyright © ITmedia, Inc. All Rights Reserved.