連載
» 2004年04月02日 00時00分 UPDATE

チューニングのためのJava VM講座(後編):ガベージコレクタの仕組みを理解する (1/2)

[猪口聖司,日本HP]

J2EEがミッションクリティカルな分野に適用されるようになり、Javaのパフォーマンスチューニングの重要性はさらに高まっています。パフォーマンスチューニングにはさまざまなパラメータがありますが、中でもJava VMに関連するチューニングの効果は大きいといわれています。本稿は、Java VMに関連するチューニング手法を学ぶための前提知識を提供することを目的にしています(編集部)。

本記事は2004年に執筆されたものです。Javaチューニング全般の最新情報は@IT キーワードINDEXの「Javaパフォーマンス管理」をご参照ください。


 ガベージコレクション(Garbage Collection:以下GC)と聞くと、「プログラマの煩雑なメモリ管理作業を軽減してくれるのはいいけど、アプリケーションの応答時間を遅らせたり、スループットを低下させたりして、パフォーマンスの観点からは非常に困ったものだ」というイメージを持つ人も多いのではないでしょうか。

 GCはJava HotSpot仮想マシン(Java HotSpot Virtual Machine:以下JVM)の主要基本機能の1つです。最近ではGC技術も進歩し、アプリケーションのパフォーマンス劣化の問題もかなり解消されています。ただし、そのためにはJVMが適切にGC処理を行うように設定する必要がある場合もあります。

 Javaプログラマの中には、普段はGCのようなJVMの内部処理にはそれほど注意を払っていない人がいるかもしれません。しかし、信頼性が高く、パフォーマンスの良いJavaシステムを構築するためには、JVM内部のメモリ管理機能であるGCについてその仕組みを把握しておくことは重要です。トラブル対応の観点からも、メモリ関連の障害(OutOfMemoryErrorの発生など)に対して状況を素早く把握し、問題の原因を突き止める場合にGCの知識は役立ちます。

 では、JVMのGCの特徴から見ていきましょう(この記事はJVMのバージョン1.4.xをベースに書いていますが、内容の大部分はバージョン1.3.xにも当てはまります)。

GCの役割とメリット

 CやC++言語では、プログラマがオブジェクトに割り当てたメモリ領域は、不要になった時点でプログラマが責任を持って明示的に解放処理を行う必要があります。そのため、解放を忘れたことによるメモリリークや、誤ったメモリ解放によるアプリケーション停止や暴走が発生しやすいわけです。

 これらメモリ解放の問題に起因するバグは、プログラムコード上でエラーとなっている場所と本当の原因個所が一致しなかったり、再現性がないためにデバッグは困難であったりするケースが多く、安全でかつ長時間安定動作させる必要があるサーバアプリケーションの開発には大きな問題となります。

 それに対しJava実行環境では、GCが不要になったメモリ領域を解放するため、プログラマが明示的にメモリ解放の処理を書く必要がありません。すなわち、メモリ解放におけるミスが人為的に発生することがないため、信頼性、安全性の高いアプリケーション開発を行うことが可能になります。

 Javaによる開発では、GCによるメモリ管理機能のメリットを亨受できる一方で、GCの挙動を把握することは重要なことです。なぜならば、GCがアプリケーションのスループット低下やレスポンスタイムの遅延といったパフォーマンスボトルネックの要因になる可能性があるからです。もしそのような事態になった場合はGCの特性を考慮してボトルネックの解消を行う必要があります。

 特にメモリサイズが大きく多くのCPUを使用する大規模なシステムでは、GCによるパフォーマンスの影響度は大きなものとなります。GCが大きなメモリ領域を複数のCPUで効率よく処理することが課題です。その対策としてJVM 1.4.1からはGC作業を複数スレッドで同時に行うパラレルGCや、GC作業の多くをアプリケーションスレッドと同時に行うコンカレントGCといったものも実装されています。これについては後半で紹介します。

メモリ解放の仕組み

 GCは実行時にヒープ内のオブジェクトが必要かどうかを判断し、不要なオブジェクトのメモリ領域を解放します。しかし、ある時点ですべてのオブジェクトに対し必要であるかどうかの判断を正確に行うことは困難です。

 必要なオブジェクトをライブオブジェクトと呼びます(ここではライブオブジェクトを広義に定義しています。この定義以外に単に参照でたどれるだけでなく、実際にアプリケーションに必要なオブジェクトを指すもっと狭い意味で使う場合もあります)。現在のGCは、ルート集合と呼ばれる参照の基となるオブジェクトから参照をたどっていき、参照でつながっているオブジェクト群を必要なオブジェクト(ライブオブジェクト)と判断します。非ライブオブジェクトのメモリ領域はGCによって解放されます。ルート集合は実装にも依存しますが、スレッドスタック内の参照変数などから構成され、アプリケーションの実行とともに変化しますが、常にアプリケーションからのアクセスが可能なものです。

図1 ルート集合から参照されるライブオブジェクトは必要なオブジェクトと判断される。それ以外(図の右下の2つのオブジェクト)のオブジェクトは非ライブオブジェクトとしてGCによって解放される 図1 ルート集合から参照されるライブオブジェクトは必要なオブジェクトと判断される。それ以外(図の右下の2つのオブジェクト)のオブジェクトは非ライブオブジェクトとしてGCによって解放される

オブジェクトを管理する仕組み「世代別GC」

 現在のJVMは世代別GCと呼ばれるGCを実装しています。一般に、生成されたオブジェクトの多くは短期間だけ必要である法則があります。そこで、オブジェクトの存在期間(世代)に注目し、寿命が短いオブジェクト(短命オブジェクト)と寿命が長いオブジェクト(長命オブジェクト)について、それぞれに対してその特性を生かし、全体的に効率よく処理を行うものが世代別GCです。

 JVMが生成したオブジェクトを割り当てるメモリ領域をヒープ領域といいます。ヒープ領域には短命オブジェクトを主に割り当てる領域(New世代領域)と、長命オブジェクトを主に割り当てる領域(Old世代領域)があります(ヒープにはそのほかにPermanent世代領域がありますが、今回の記事では対象としていません)。New世代領域はさらに、Eden領域、From領域、To領域から構成されています(From領域とTo領域は同じサイズです)。

図2 世代別GCの各領域 図2 世代別GCの各領域

 実際に世代別GCでオブジェクトがどのように管理され、どのようにGCが実行されているのかを具体的な例で見ていきましょう(概要を把握しやすいように単純化しているため細部は正確でない部分があります)。

New世代GC

 アプリケーションが開始され、newメソッドなどによってオブジェクトが生成されると、そのオブジェクトはまずEden領域に割り当てられます。アプリケーションが進行するに従って、順次Eden領域にオブジェクトが割り当てられていきます。やがてEden領域がいっぱいになり、新たにオブジェクトを割り当てる領域が不足するようになります。これがトリガとなりNew世代領域を対象としたGC(New世代GC)が実行されます。

 New世代GCではEden領域内のライブオブジェクトが検出され、To領域にコピーされます(ライブオブジェクトでないオブジェクト領域は解放されます)。このときオブジェクト間の参照整合性を保ちながら安全にオブジェクトを移動させるため、New世代GCの実行中はアプリケーションスレッドをすべて停止させる必要があります。New世代GCが終了すると、アプリケーションスレッドは再開されます。また、New世代GCごとにFrom領域とTo領域の役割が交代するので、From領域とTo領域の名前を形式的に入れ替えます。

図2 New世代GCの動作(1回目) 図2 New世代GCの動作(1回目)

 アプリケーションが再開されると、再びGC後のEden領域へオブジェクトの割り当てが行われ、Eden領域がいっぱいになると再びNew世代GCが起こります。先ほどは、Eden領域からTo領域へのライブオブジェクトのコピーのみでしたが、2回目以降はFrom領域のライブオブジェクトもTo領域にコピーされます。当然、コピーされなかったFrom領域内のオブジェクトの領域は解放されます。そのほかは1回目のNew世代GCの処理と同様です。

図3 New世代GCの動作(2回目) 図3 New世代GCの動作(2回目)

 New世代GCでは、オブジェクトの移動は単純なコピー処理(Copying方式)です。しかもNew世代GC後はEden領域、To領域は空なので、メモリの連続領域の確保が容易にできオブジェクトの割り当て、移動が高速に行えます(ただし、空のメモリを用意するため、処理対象のオブジェクト量に対して多くのメモリを消費します)。

 New世代GCではアプリケーションの停止時間は短く、アプリケーションへのパフォーマンスへの悪影響は比較的少ないといえます。寿命が短いオブジェクト(短命オブジェクト)に関しては、このNew世代GCで処理を行うように設定を行うのが効率の良いGCを実行させるポイントの1つです。

Old世代GC

 New世代GCを繰り返すと長命オブジェクトはずっとFrom領域とTo領域を行ったり来たりすることになってしまい、処理コストの面でも好ましくありません。そこで、長命オブジェクトはNew世代領域からOld世代領域に移動させます。そのためには各オブジェクトに年齢属性を持たせます。オブジェクトの年齢というのは、オブジェクトが経験したGCの回数です。すなわち、Eden領域に生成されたオブジェクトは年齢0です。その後オブジェクトがNew世代GCをくぐり抜けるごとに年齢が1つずつ増加していきます。やがて、ある年齢に達したオブジェクトはNew世代領域(From領域)からOld世代領域に移動します(大きなオブジェクトは直接Old世代領域に割り当てられます)。

 アプリケーションの処理が進むと、長命オブジェクトがOld世代領域に割り当てられていき、やがてOld世代領域の空き領域が不足してきます。この時点でOld世代領域を対象にOld世代GCが起こります。Old世代GCはOld世代領域内でライブオブジェクトを見つけ、非ライブオブジェクトのメモリ領域を解放し、フラグメンテーション(断片化)を生じさせないようにライブオブジェクトを連続領域に移動させます。このOld世代GCは、mark-sweep-compact方式といわれるものでOld世代領域内の全オブジェクトのうちライブオブジェクトにマークし(mark)、非ライブオブジェクト領域を解放し(sweep)、最後にメモリのフラグメンテーションを解消するためにライブオブジェクトを集めてコンパクト化します(compact)。

 Old世代GCはNew世代領域GCに比べて、コストが高い処理で、アプリケーションの停止時間もより長くなります。このOld世代領域GCが頻発するとアプリケーションのパフォーマンス悪化が激しくなるので注意が必要です。基本的にはOld領域サイズは長命オブジェクトの量を基にサイズを設定する必要があります。

       1|2 次のページへ

Copyright© 2017 ITmedia, Inc. All Rights Reserved.

@IT Special

- PR -

TechTargetジャパン

この記事に関連するホワイトペーパー

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。