ここが大変だよ、JavaのGC/メモリ管理:現場にキく、Webシステムの問題解決ノウハウ(6)
本連載は、日立製作所が提供するアプリケーションサーバ「Cosminexus」の開発担当者へのインタビューを通じて、Webシステムにおける、さまざまな問題/トラブルの解決に効くノウハウや注意点を紹介していく。現在起きている問題の解決や、今後の開発のご参考に(編集部)
Webシステムを安定稼働させるには、考慮しなければならないことは数々存在する。システムの適切なサイジングを行うことも、その1つだ。
今回は、その中でもJavaVMのメモリのサイジング(見積もり)とGC(ガベージ・コレクション)に着目して説明する。
現象の見え方
今回は、以下の問題についての話だ。
性能が安定しない。普段は軽い処理なのに、ピーク時などにレスポンス遅延が発生する。
メモリサイジングをミスした場合、業務ピーク時のGCの多発や長期化からこの現象に陥る場合がある。CPU利用率が100%に張り付いて性能が出ない場合は、GCが原因かを疑う必要がある。その場合、まずはGCログを参照し、GCの処理時間や発生頻度から1分ごとのGC処理の占有率を確認してみるとよい。
占有率が大きい場合、アプリケーションの処理やメモリのサイジングを見直す必要がある。以降では、GCを考慮したメモリのサイジングについて説明する。
JavaVMのメモリ空間はどうなっているのか
初めに、Cosminexusの場合を例に、JavaVMのメモリ空間の構成を示す。
一般に、JavaVMのメモリ空間は、JavaVMが管理するJavaVM固有領域と「Cヒープ」などのOSネイティブの領域で構成される(Cosminexusの場合は、このほかに後述のFull GCレス機能に対応する「Explicitヒープ」が存在するという)。
JavaVM固有領域は、Javaオブジェクトを配置する「Javaヒープ領域」と、ロードされたclassなどの情報を格納する「Permanent領域」に分けられ、JavaVMが世代別GCを採用する場合、Javaヒープはさらに、「Eden」「Survivor」「Old」領域に分けられる。
生成されたオブジェクトはまずEden領域に配置される。GCのタイミングで使用済みのオブジェクトは回収されるが、使用中のオブジェクトはSurvivor領域に移動する。そして、生存期間の長いオブジェクトは、何度かのGCの後でOld領域へと移動する仕組みとなっている。
GC発生の要因をおさえる
JavaVMは、メモリ領域の利用状況に応じて使用済みメモリ領域を回収する。これがGCである。世代別GCを採用する場合、GCには「Copy GC」「Full GC」がある。
Copy GCは「New領域」(Eden+Survivor)を、Full GCではJavaVM固有領域全体を対象に、使用済みメモリ領域を回収する。そのため、GCの多発や長期化を防ぐには、それぞれのGCの発生要因を把握し、適切なサイジングを施すことが必要となる。
例として、CosminexusでのGCの主な発生要因を見てみよう。
- Copy GCの発生要因
- Eden領域へのJavaオブジェクトの配置で空き領域が不足
- Full GCの発生要因
- 上記1−1のときに、Old領域の最大値に対する未使用メモリサイズよりもNew領域で使用しているメモリサイズが大きい
- New領域とOld領域のそれぞれの未使用メモリサイズよりも、大きなメモリサイズのJavaオブジェクトの配置要求が発生
- Copy GC実施の結果、確保済みのOld領域での未使用メモリサイズが10000bytesを下回る
- Copy GC実施の結果、Old領域にオブジェクトが移動し、確保済みOld領域の拡張が発生
- Permanent領域に割り当てたいメモリサイズが確保済みPermanent領域の未使用メモリサイズを上回る
Permanent領域は、Webシステムの稼働中は変動が少ない。そのため、主に、Javaヒープを対象に見積もっていき、Full GCの発生要因1と2が発生しないようにサイジングする必要がある。
また、Old領域の拡張が発生しないように、Javaヒープ領域の初期値と最大値は同じ値にすることを推奨する。
メモリサイズの見積もり方針
一般に、Copy GCに比べて、Full GCは実行に時間がかかる。そのため、Full GCはなるべく発生させず、使用済みメモリ領域はCopy GCでうまく回収させることが安定稼働のポイントとなる。
そのためには、Full GCの発生間隔を考慮してOld領域のサイズを見積もることが重要だ。ポイントを以下に示す。
【1】Old領域のメモリサイズの見積もり
Old領域のメモリサイズは、「アプリケーションが使用するメモリサイズ」+「Java EEサーバで使用するメモリサイズ」+「New領域のメモリサイズ」で見積もる。
セッション情報の蓄積量からサイジングを考える
この中で重要なのは、「アプリケーションで使用するメモリサイズ」だ。このメモリサイズはOld領域に移動する「生存期間が長いオブジェクト」のサイズから見積もることとなる。
Webシステムの場合、トランザクション処理で利用されるオブジェクトは確保しても、すぐに捨てられるのに対し、セッション情報などはクライアントのログインからログアウトまで、ずっと利用されるため、生存期間が長い。そのため、セッション情報の蓄積量からアプリケーションで使用するメモリサイズを見積もるとよい。
具体的には、セッション情報のサイズと毎秒の想定ログイン数から毎秒増加するセッション情報のメモリサイズを算出し、すべてがOld領域に移動した場合でも、Full GCの発生許容間隔を満たすように見積もることになる。
最大ログイン数に関しては、さらに考慮が必要
ただし、最大ログイン数に関しては、さらに考慮が必要だ。Full GCのタイミングでは、最大ログイン数分のセッションは使用中で、Full GC後も残る場合がある。そうなると、次回のFull GCは、許容時間よりも早く発生してしまう。そのため、最大同時ログイン数分のメモリサイズを余分に確保する必要がある。
Old領域全体では「Java EEサーバで使用するメモリサイズ」+「New領域のメモリサイズ」が追加で必要となる。「New領域のメモリサイズ」は、先ほど述べたFull GC発生要因の1に対する備えである。
JavaVMは、GCでNew領域からOld領域に多くのオブジェクトが移動した場合でも、メモリ不足とならないように、Old領域の空き領域がNew領域サイズよりも少ない場合には、Full GCを実行する。そのため、New領域のメモリサイズ分は余分に見積もる必要がある。
【2】New領域のメモリサイズの見積もり
New領域では、Survivor領域の見積もりが大切だ。Survivor領域での見積もりのポイントは領域のサイズとOld領域にオブジェクトが移動する年齢だ。
【2−1】Survivor領域の見積もり
Survivor領域のサイズが小さく、Copy GCのタイミングでSurvivor領域があふれると、Old領域にオブジェクトが移動する。生存期間の短いオブジェクトがOld領域に移動すると、Full GCの要因となってしまう。そのため、Survivor領域は十分なサイズが求められる。
一方で、いつまでもSurvivor領域にオブジェクトが残っていると、Copy GCの処理時間は長くなる。そのため、オブジェクトの年齢(生き残ったCopy GCの回数)が、ある“しきい値”を超えると、そのオブジェクトはOld領域に移動するようになっている。
そのため、以下をチェックする必要がある。Survivor領域があふれて、Eden領域からOld領域に直行することがないように注意が必要だ。
- Copy GC後のSurvivor領域のメモリ利用率は100%になっていないか
- 適切な年齢でOld領域にオブジェクトを移動させているか
見積もりの考え方としては、以下のようにするとよい。
- 領域サイズは、リクエストやレスポンス処理で必要となるメモリの最大サイズ(生存時間が短いオブジェクトのサイズ)を考慮して見積もる
- Old領域に生存期間の短いオブジェクトが多く移動していたり、Copy GCの処理時間が長い(例:Full GCよりもCopy GCの処理時間が長くなる)場合は、「TargetSurvivorRatio」オプションの値を見直す
ちなみにCosminexusでは、Survivor領域におけるOld領域への移動年齢のしきい値はCopy GCごとに動的に変動するという。MaxTenuringThresholdオプションの値を超える年齢のオブジェクトは必ずOld領域に移動するが、しきい値は、Survivor領域のメモリ利用率が「TargetSurvivorRatio」オプションの値に近くなる年齢を算出し、次回Copy GCでの移動のしきい値として利用される。
また、Survivor領域でのオブジェクトの年齢分布を調べるには、Cosminexusでは独自のオプション「-XX:+HitachiVerboseGCPrintTenuringDistribution」を利用するとよいという。Copy GCでのSurvivor領域の使用状況がログに出力できる。世代を考慮した累計のメモリ使用量が出力されるため、年齢分布が把握できる。
【2−2】Eden領域のサイズの見積もり
Eden領域のサイズは、Copy GCの頻度に直結する。当然ながら、サイズが大きいとCopy GCの発生間隔は長くなる。Eden領域のサイズは、システムのCopy GC許容間隔の条件を満たすように設定する。
1トランザクションで使用するメモリ使用量とシステムのTPSから、毎秒のメモリ使用量を求めることができる。そこで、Eden領域がいっぱいになるまでの時間がCopy GCの許容間隔よりも大きくなるように見積もる。
なお、Copy GCの処理時間そのものはオブジェクトのサイズよりも個数に影響される。
このように、GCを考慮してメモリサイズを見積もることとなる。現実的なラインとしては、Copy GCが10〜20回の間にFull GCが1回発生するぐらいが目安だ。当然のことだが、見積もり後にGCの間隔が想定と違う場合には、各領域のメモリサイズを再度調整することとなる。
適切なメモリサイジングでシステム安定稼働の実現を
Webシステムでは、Full GC発生の一番の要因は、HTTPセッションに関連するオブジェクトだ。セッションと関連するオブジェクトはログインからログアウトまで生存し、生存期間が長い。そのため、Old領域に蓄積していきFull GC発生の要因となるのだ。
このようなGC発生の要因を考慮し、高い精度のメモリサイジングを行うことで、システムの安定稼働へとつなげてみてはいかがだろうか。
ちなみにCosminexusでは、Full GCレス機能を備え、HTTPセッションのような一定期間存在するオブジェクトを、Explicitヒープで管理することが可能だ。これにより、Old領域へのセッションオブジェクトが移動することを防止し、Full GCの発生を抑止できるという(参考:日立がアプリサーバ新版、Full GC回避し「世界を止めない」)。最新版のV8.5では、より機能が強化され、アプリケーションや特定フレームワークでOld領域増加の要因がある場合でも、適用できるようになったそうだ。
Copyright © ITmedia, Inc. All Rights Reserved.
関連記事
- 第1回 Webアプリの問題点を「見える化」する7つ道具
- 第2回 “Stop the World”を防ぐコンカレントGCとは?
- 第3回 【実録ドキュメント】そのログ本当に必要ですか?
- 第4回 DBアクセスのトラブルは終盤で発覚しがち……
- 第5回 OutOfMemoryエラー発生!? GCがあるのに、なぜ?
- 第6回 【真夏の夜のミステリー】Tomcatを殺したのは誰だ?
- 第7回 【トラブル大捜査線】失われたコネクションを追え!
- 第8回 肥え続けるTomcatと胃を痛めるトラブルハッカー
- 第9回 JavaのGC頻度に惑わされた年末年始の苦いメモリ
- 第10回 ThreadとHashMapに潜む無限回廊は実に面白い?
- 第11回 スレッドダンプの森で覚えた死のロックへの違和感
- 第12回 アプリ開発でも、よ?く考えよう。キャッシュは大事だよ
- 第13回 DB操作の“壁”を壊すJPAが起こした「赤壁の戦い」
- 第1回 クラスタ化すると遅くなる?
- 第2回 キャッシュが性能劣化をもたらす謎を解く
- 第3回 クラスタは何台までOK?
- 第4回 マルチスレッドのいたずらに注意
- 第5回 クラスタによるアプリケーションの動的アップデート
- 第6回 APサーバからの応答がなくなった、なぜ?
- 第7回 低負荷なのにCPU使用率が100%?
- 第8回 文字化け“???”の法則とその防止策
- 第9回 メモリは足りているのに“OutOfMemory”のなぞ
- 第10回 レスポンスキャッシュでパフォーマンス向上
- 第11回 JDBC接続を高速化?PreparedCacheの活用
- 第12回 ブラウザキャッシュでパフォーマンス向上
- 第13回 ファイルアップロード/ダウンロードに潜むわな
- Eclipse上でプロファイリングを実現する
- JMeterによるWebサーバ性能評価の勘所