Java GCチューニング

あるプロジェクトにて、いつものように朝出勤するとサポート担当者が
慌ただしく電話対応に追われていた。
話を伺ってみると、昨日未明にアプリケーションサーバがシステムダウンが発生したとのこと。
その場は、サーバをリブートしてシステムを復旧することができたが、
原因は調査しなくてはならない。
ログを解析すると、メモリリークが発生したことが原因なのはすぐに分かったが、
メモリリークが発生した原因が分からない。
夜間バッチの処理中にメモリリークが発生しているが、
日次処理であり今までは問題なく稼働していた。
急激にトランザクションデータが増加したわけでもない。

そもそも、メモリリークは何故発生するのだろうか。
Javaの一番の特徴は、GC(ガベージ・コレクション)にあると言っても過言ではない。
通常、開発者はメモリ管理を意識してコーディングする必要があるが、
Javaに関しては開発者に代わってJVMが任意のタイミングでGCを発生させてくれるので、
基本的に開発者はメモリ領域を意識せずにコーディングに専念できる。
他言語からJava開発にシフトしてきた開発者の中には、
メモリ管理をせずにはいられない人もいて、明示的にGCを発生させるコードを記述する人もいる。
その際は、使用したオブジェクトを初期化しないとGCの対象にはならない。
どういうことかと言うと、JVMは現時点で利用されているメモリが必要かどうか判断できない。
メモリに格納されているデータを全てクリアしてしまうと、
今後の処理で必要となるデータまで削除されてしまう可能性があるからである。
Webアプリケーションでは、アプリケーションレベル・セションレベルのデータを
保持しておかなくてはならない。
では、GCが発生する任意のタイミングとはいつなのか。
JDK1.2以降では、ヒープ領域としてNEW・OLDの各領域が用意されていて、
NEW領域がいっぱいになった段階でGC(ScavengeGC)が走る。
OLD領域には、ScavengeGCで32回生き残ったNEW領域のオブジェクトが移行する。
そして、OLD領域がいっぱいになるとFullGCが走る。
これがメモリリークの原因である。

今回の事例で何故メモリリークが発生したのか。
まず、アプリケーションサーバが起動したタイミングでシステム全体で利用するリソースは
アプリケーションレベルで保存されるので、後にOLD領域に移行することが確定する。
次に通常業務はトラフィックな状態になると、セションデータもOLD領域に移行しやすくなる。
さらに、日次業務である夜間バッチは大量データを処理する為、一時データもOLD領域に
移行しやすくなる。
日々、この状態が続くとOLD領域がいっぱいになり、FullGCが頻発して最終的には
OutOfMemoryでシステムダウンすることとなる。

メモリリークを回避するにはどうすればいいのか。
全体のヒープ領域を拡張するという方法も考えられるが、
コストが掛かる上に成果が上がらないということも考えられるので
安易に拡張すべきではない。
NEW領域のサイズを変更してはどうだろうか。
拡大させると性能を向上させるというメリットがあるが、
メモリ効率を低下させるというデメリットも併せ持つ。
最大限に活かすには、ヒープ全体の1/3~1/4が妥当である。

NEW領域のEdenとSurvivor(From,To)の比率を変えてみてはどうだろうか。
オブジェクトは、New:Eden→New:Survivor(From) →New:Survivor(To)→OLD
の経路を辿るのだが、Survivor領域が小さいとScavengeGCが32回走るまでに
OLD領域に移行してしまう。
アプリケーションの特性を考えて、この比率を変えていくのが得策である。

どうやって解消したのか。
GCのチューニングで解決するのがベストであるが、
同様の事象が発生することも懸念されるので定期的(業務時間外で日次or週次のタイミング)に
アプリケーションサーバをリブートするという運用で落ち着いた。
システム稼働当初は、チューニングを行った上でGCの挙動を監視する運用を怠ってはいけないと
痛感させられる出来事であった。

<実行オプション>
-Xms : 初期ヒープサイズ(全体)
-Xmx : 最大ヒープサイズ(全体)
-Xmn(-XX:NewSize) : New世代領域サイズ
-XX:MaxNewSize : New世代領域サイズ
-XX:NewRatio : New世代領域とOld世代領域の比率(Old世代領域/New世代領域)
-XX:SurvivorRatio : New世代領域とSurvivor領域の比率(Eden領域/From領域)
-XX:TargetSurvivorRatio : New世代領域GC後のFrom領域内オブジェクトの割合目標

-verbose:gc(-verbosegc) : GC情報(簡易)
-Xloggc:filename : GC情報(ファイルへ出力)
-XX:+PrintGCDetails : GC情報(詳細)
-Xverbosegc : GC情報(詳細)を出力(HP-UX JVMのみ)
-XX:+PrintTenuringDistribution : オブジェクトの年齢情報
-XX:+TraceGen0Time : New世代領域の累積GC時間、GC回数、平均GC時間
-XX:+TraceGen1Time : Old世代領域の累積GC時間、GC回数、平均GC時間
-XX:+PrintGCTimeStamps : GCのタイムスタンプ
-XX:+PrintHeapAtGC : GC前後の詳細なヒープ情報

<GCログ解析ツール>
                               GCViewer