本連載では、現場でのエンジニアの経験から得られた、アプリケーション・サーバをベースとしたWebシステム開発における注意点やヒントについて解説する。巷のドキュメントではなかなか得られない貴重なノウハウが散りばめられている。読者の問題解決や今後システムを開発する際の参考として大いに活用していただきたい。(編集局)
アプリケーション・サーバから応答がない、いわゆる無応答状態は、ベンダのサポートセンターに寄せられる質問でも数が多いといわれている。無応答状態の原因の多くはGC(ガベージ・コレクション)にあり、これはGCチューニングにより解消可能だ。今回の記事では、GCチューニングにより無応答状態を解決する道のりを紹介していく。
あるとき、長時間レスポンスが返ってこないという事象が発生した。定期的な応答時間の監視から、無応答状態はアプリケーション・サーバを起動してから数時間経過すると発生し、数分間無応答状態が続いた後に再び正常に処理を開始することが分かった。
筆者はこの現象を見て、無応答が数分間で終わっていることからガベージ・コレクション(GC)が原因であるとの仮説を立てた。GC実行中、アプリケーション・サーバのCPUはGCのためだけに使用される。GCに数分間かかった場合、その間アプリケーション・サーバは無応答状態となる。
原因特定のために、まず、Javaの起動時にGCログ取得オプションを設定することにした。GCログ取得オプションには、以下のものが存在する。
(a)は、世代別GC(後述)についてのかなり詳細な情報まで取得することができる。(b)は、SolarisのJDK 1.2だけにある隠れオプションで、2度同じ記述をするのがポイントだ。世代別GCの詳細情報を取得できるが、(a)とはフォーマットが異なる。また、標準エラー出力に出力される。(c)はそのほかのJDKで有効なオプションである。ただ、このオプションの場合、世代別GCの情報はほとんど得られない。また、標準エラー出力に出力される。
(b)(c)は、標準エラー出力にメッセージが出るため、
java … > console.log 2>&1
のようにして、ファイルに書き出す必要がある。いずれのオプションも、ログファイルが大きくなるが、オーバーヘッドは小さいので、最初から指定しておくべきである。
なお、HP社より無償で提供されているHPjtuneというツールを用いると、上記(a)のログをグラフィカルに解析することができる。また、ヒープパラメータ(後述)を変更したときのGC発生の様子をシミュレーションすることができる。
該当時刻のGCログを見たところ、Full GC(後述)が走っていることが分かった。その処理時間は数分であり、無応答の時間とほぼ一致していた。つまり、無応答の原因がGCであるという仮説は間違っていなかった。
JDK 1.1のGCは、「マーク・コンパクト」という方式であった。この方法は、ヒープ領域がいっぱいになったときに一気にGCを行うというものである。しかしこの方法では、ヒープサイズが大きくなるに従いGC時間が長くなっていくという欠点がある。
JDK 1.2以降では、世代別GCという方式が採られている。世代別GCでは、ヒープ領域が「新しいオブジェクト(NEW)」領域と「古いオブジェクト(OLD)」領域に分けられている。新規に作成されたオブジェクトはNEW領域に配置され、NEW領域がいっぱいになった段階で、NEW領域内部だけのGCが走る(これをScavenge(廃品回収型)GCと呼ぶ)。Scavenge GCは「コピーイング」方式を採っており、「マーク・コンパクト」方式より低負荷・高速であるが、メモリ効率が悪いという欠点がある。
Scavenge GCで一定回数(32回)生き残ったオブジェクトは、「殿堂入り(Tenure)」して、OLD領域に移る。そして、OLD領域がいっぱいになると「マーク・コンパクト」方式によるFull GCが走ることになる。
一時的に作成したような寿命の短いオブジェクトは、通常32回のScavenge GCの間に消滅するので、殿堂入りしてOLD領域にいくことはまずない。コネクションプールやスレッドプールなどのリソースや、HttpSessionに格納されたオブジェクト、キャッシュなどが殿堂入りしてOLD領域に入る候補となる。また、それ以外では、あってはならないことだが、メモリリークしたオブジェクトもOLD領域に入ってしまう。
OLD領域は、寿命の長いオブジェクトの置き場所である。OLD領域がいっぱいになると、Full GCが走り、長時間の無応答状態が起きてしまうため、ここがいっぱいになってしまうことは避けなければならない。
Full GCを発生させないためには、2つのアプローチがある。
(1)の無駄なオブジェクト生成を最小限に抑えるための手法は、今回の本論から外れるので割愛し、ヒープパラメータのチューニングについて説明する。
これからヒープパラメータのチューニングを行っていくわけであるが、その前に、世代別GCについてもう少し詳細に説明していく。前述のように、世代別GCではヒープ領域がNEW領域とOLD領域に分けられている。NEW領域はさらに細かく分けられており、「Scavenge GCを一度も経験していない、生まれたばかりのオブジェクト(Eden)」領域と「1度以上Scavenge GCを経験して生き残ったオブジェクト(Survivor)」領域の2つが存在する(さらに、Survivor領域は、FromとToの2つに分けられる)。Eden、Survivor、OLDという3つの領域のバランスを調節することが、ヒープパラメータのチューニングということになる。
実際には、以下の3つのヒープパラメータを調節していくことになる。
最初に思いつくのは全体のヒープサイズを拡張してしまおうという考えだ。しかし最初からこれを行うのはお勧めしない。なぜなら、ヒープサイズを拡張させた後で結局Full GCが発生してしまったら、さらに無応答の時間が長くなってしまうことになるからだ。全体のヒープサイズを変更するのではなく、それ以外のヒープパラメータを調整し、それでも解決しない場合に初めて全体のヒープサイズを拡張しよう。
では、NEW領域のチューニングはどうだろうか。前述のとおり、NEW領域がいっぱいになるとScavenge GCが走る。Scavenge GCは、負荷が低く、処理時間が短いというメリットがある。しかし、メモリ効率が多少悪くなる。すなわち、NEW領域を大きくすると、性能は向上するが、メモリ効率が下がる。一般に、アプリケーション・サーバでは全体のヒープサイズの1/4〜1/3程度に設定するのがよいとされており、ここでもそれに従った。
HP-UXのJDKのオプションである-Xverbosegcを用いると、Scavenge GC(およびFull GC)が走るごとにさまざまな情報がGCログに出力される。ログの内容は以下のとおりだ。
さて、Max Tenuring Threshold(以下Tenure)について簡単に説明する。世代別GCでは、通常32回のScavenge GCを生き抜いたオブジェクトのみが殿堂入りしてOLD領域に移る。ところが、32回のScavenge GCを生き抜く前に(しばしば1回目のScavenge GCで)殿堂入りしてしまうオブジェクトがたまに出てきてしまう。これは、初めてのScavenge GCで生き残ったオブジェクトがEden領域からSurvivor領域に移ろうとしたのに、Survivor領域がいっぱいだったときに、やむを得ずOLD領域に移るのが原因である。
通常Tenure=32であるが、1回目のScavenge GCで殿堂入りするオブジェクトがあったとき、Tenure=1になる。このとき、まだ殿堂入りしてはいけないオブジェクトがどんどんOLD領域に入っていき、Full GCが起きやすくなる。すなわち、Tenure=32を死守することが求められる。
Tenure=32を死守するためにはどうしたらいいだろうか。前述のように、生き残ったオブジェクトがEden領域からSurvivor領域に移る際に、Survivor領域がいっぱいだとTenureの値が下がってしまうことが分かる。すなわち、Eden領域のサイズに比べてSurvivor領域が小さすぎることが原因のようだ。
このケースでは、SurvivorRatio=8としており、Survivor領域のサイズはEden領域の1/8しかなかった。そこで、SurvivorRatioの値を2に変更したところ、Tenureの値は32から下がることはなくなり、Full GCは発生しなくなった。
最後に、Permanent領域について述べる。Permanent領域は、NEW領域・OLD領域とは異なる領域で、リフレクション情報を格納する。JSPはリフレクション情報を利用するので、この領域を大きめに取る必要がある。Permanent領域がいっぱいになったときもFull GCが発生するので注意しなければならない。なお、Permanent領域は、-XX:PermSize、-XX:MaxPermSizeのオプションで設定する。
池田 貴之(いけだ たかゆき)
現在、株式会社NTTデータビジネス開発事業本部に所属。 技術支援グループとして、J2EEをベースにしたWebシステム開発プロジェクトを対象に、技術サポートを行っている。特に、性能・信頼性といった方式技術を中心に活動中。
Copyright © ITmedia, Inc. All Rights Reserved.