Javaの理論と実践: 1.4.1 JVM中のガーベジ・コレクション

世代別と並行ガーベジ・コレクション

今回、Brian Goetzは実際どのように1.4.1JVMがガーベジ・コレクションを処理しているのか、マルチプロセッサ・システム用の新しいガーベジ・コレクションのオプションのいくつかを含めて調査します。

Brian Goetz (brian@quiotix.com), Principal Consultant, Quiotix

Brian Goetz は18 年間以上に渡って、専門的ソフトウェア開発者として働いています。彼はカリフォルニア州ロスアルトスにあるソフトウェア開発コンサルティング会社、Quiotixの主席コンサルタントであり、またいくつかのJCP Expert Groupの一員でもあります。2005年の末にはAddison-Wesleyから、Brianによる著、Java Concurrency In Practiceが出版される予定です。Brian著による有力業界紙に掲載済みおよび掲載予定の記事のリストを参照してください。



2003年 11月 25日

参照カウンタ、コピー、mark-sweepおよびmark-compactの典型的なガーベジ・コレクションはある状況において、利点と欠点を持っています。例えば、多くのオブジェクトがガーベジである場合、コピーをうまく行えますが、長期にわたって存在したオブジェクト(コピーを繰り返し行ったオブジェクト)ではうまく行えません。反対に、mark-compactは長期にわたって存在したオブジェクト(それらを一度だけコピーして)でとてもうまくコピーを行えますが、多くの短命なオブジェクトでそれほどよくコピーを行えません。世代別ガーベジ・コレクションと呼ばれる1.2以降のJVMによって使用される技術は、一石二鳥を得るためにこれらの2つの技術を組み合わせ、非常に少ないメモリでオブジェクトのメモリ割当を提供しています。

古いオブジェクト、若いオブジェクト

どんなアプリケーションヒープでも、オブジェクトが作られた後、いくつかのオブジェクトはガーベジになります。また、あるものは長期にわたって存在した後、ガーベジになります。また、他のものはプログラムの実行の間、存在し続けます。Java言語を含む多くのオブジェクト指向言語で、98%ものオブジェクトの大多数は、オブジェクトの初期の測定基準によっては、若いうちに存在しなくなるという証明をしました。掛け時計の秒や、オブジェクトがアロケートされてからメモリ管理サブシステムによって割り振られた合計バイトや、ガーベージコレクション数でオブジェクトの世代を計測することができます。たとえあなたが測定しても、結果は大多数のオブジェクトは若い世代でなくなるという、同じことを示すでしょう。ほとんどのオブジェクトが若い世代で存在しなくなるという事実は、コレクター選択のための重要性を意味しています。特に、コピー・コレクターは全くデッドオブジェクトを訪れないので、大多数のオブジェクトが若い世代で非稼動となる場合に有効です。生存しているオブジェクトを他のヒープ領域にコピーし、残りの全スペースを再利用します。

それらの最初のコレクションを過ぎて残存するオブジェクトのうち、それらの重要な部分は、長命永久になるでしょう。様々なガーベジ・コレクション戦略は、短命や長命のオブジェクトのミックスによって非常に異なった動きをします。若い世代にデッドオブジェクトとなったものは、全くコピーされる必要がないので、ほとんどのオブジェクトが若い世代でデッドオブジェクトとなる時、copingコレクタは非常に有効です。しかし、copingコレクタは、繰り返し、1つのセミスペースから他のスペースのあちらこちらにコピーするので、長命のオブジェクトに対してあまり有効ではありません。反対に、長命のオブジェクトは、ヒープの下に蓄積され、再びコピーされる必要がないので、mark-compactコレクターは大変有効です。しかし、mark-sweepコレクタやmark-compact コレクタは、sweepフェーズの間、すべてのオブジェクトを検査しなくてはならないので、デッドオブジェクトを検査するのにもっと労力を費やします。


世代別コレクション

世代別コレクターはヒープを多数の世代に分割します。若い世代でオブジェクトを作り、コレクションのある数を生存してきたような、プロモーション基準にあうオブジェクトは、次の古い世代に促進さます。世代別コレクタは、異なる世代のために違うコレクションの方針を自由に使え、世代別にガーベジ・コレクションを別々に実行することができます。

Minorコレクション

世代別コレクションの利点のうちの1つは、一度にすべての世代をコレクティングしないで、ガーベジコレクションを短く停止することができることです。アロケーターが割付けリクエストを完了することができない場合、最初にもっとも若い世代をコレクトするminorコレクションをトリガーします。若い世代の多くのオブジェクトがすでにデッド状態となり、コピーコレクターが全くデッドオブジェクトを検査する必要がないので、minorコレクションはちょっとの間停止して、重要なヒープスペースを再利用することができます。もしminorコレクションが十分なヒープスペースを自由にすれば、ユザー・プログラムはすぐにレジュームすることができます。もし、自由にできないのであれば、十分なメモリが再利用されるまで、高い世代がコレクトされ続けます。(イベント内で、ガーベジ・コレクターは十分なコレクションの後に十分なメモリを取り戻すことができません。ヒープを拡張するか、あるいはOutOfMemoryErrorをthrowするでしょう。)

世代間の参照

コピーやmark-sweep、mark-compactのようなガーベージ・コレクションをトレースすることは、すべてルートセットからスキャニングを開始し、生存している全てのオブジェクトに到達するまで、オブジェクト間参照を全探索します。

世代別トレーシングコレクターはルートセットから始めますが、トレースされるオブジェクトグラフのサイズを減少されるような、古い世代のオブジェクトへ導く参照は探索しません。しかし、これは問題を引き起こします。--もし、古い世代のオブジェクトがルートから何か他の一連の参照によっても到達できない若いオブジェクトを参照する場合、どうでしょうか?

この問題に取り組むために、世代別コレクターは、より古いオブジェクトからより若いオブジェクトへの参照を明白に追跡し、小さなコレクターのルートセットへの参照にこれらのより古いオブジェクトからより若いオブジェクトへの参照を追加しなければなりません。古いオブジェクトから若いオブジェクトへの参照を作成するために、2つの方法があります。古いオブジェクトに含まれていた参照のうちの1つを若いオブジェクトへの参照に修正するか、他の若いオブジェクトを参照している若いオブジェクトをより古い世代へ促進するかです。

世代間の参照のトラッキング

古い世代から若い世代への参照が促進あるいはポインター修正によって作成されても、minorコレクションを実行したい場合、ガーベジ・コレクターは、古い世代から若い世代への参照の理解しやすい設定を持つ必要があります。これをする一つの方法として、古い世代をトレースすることであるが、これは明白に重要なオーバーヘッドを持っています。幾分よい方法は、若いオブジェクトへの参照を捜す古い世代のオブジェクトを直線的にスキャンすることでしょう。このアプローチはトレーシングより速く、よりよい位置関係がありますが、まだ相当な仕事です。

mutatorおよびガーベジ・コレクターは古い世代から若い世代への参照の理解しやすいリストを維持するのに、一緒に動きます。オブジェクトがより古い世代へ促進される場合、ガーベジ・コレクターはポインター修正によって作成された世代間の参照のトラッキングだけ残した促進の結果によって作成されたいくつかの古い世代から若い世代への参照に注意することができます。

ガーベジ・コレクターは、いくつかの方法で既存のオブジェクト内に保持された参照の修正によって発生する、古い世代から若い世代への参照を追跡することができます。それは、参照カウンタコレクタ内の保持する同じ方法で追跡することができるはずです。(コンパイラーは、ポインター割り当てを囲む追加の指示を生成することができました)あるいは、古い世代のヒープ上でより古いオブジェクトへの書き込みをトラップするために、仮想メモリ保護を使用することができるはずです。もしかすると、別の方法では、より有効的な仮想メモリへのアプローチは、古い世代から若い世代へのポインターを含んでいるオブジェクトをスキャンするブロックを識別するために、古い世代のヒープ内の汚れたビットのページ変更に使用することでしょう。

少し巧妙に、世代の境界をクロスするかどうかを見るために、すべてのポインター修正をトラッキングし、検査をするのを回避することは可能です。例えば、既にルートセットの一部になっているかもしれないので、ローカル変数あるいはstatic変数へのストアを追跡することは必要ではありません。(ほとんど)すべてのオブジェクトが若い世代に割当てられるので、新しく作成されたオブジェクトの単に初期化するフィールド(いわゆる初期化記憶装置)のコンストラクタ内のポインターのストアをトラックするのを回避することも可能かもしれません。どんな場合も、実行時は、古いオブジェクトから若いオブジェクトへの参照の明示的な設定を維持し、若い世代をコレクトする場合、ルートセットへこれらの参照を加えなければなりません。

図1では、矢印が、ヒープの中でのオブジェクト間の参照を表わしています。赤い矢印は、minorコレクションのためにルートセットに加えられなければならない、古い世代から若い世代への参照を表わします。青い矢印は、若い世代だけをコレクトする時、トレースされる必要がない若い世代やルートセットからの古い世代オブジェクトの参照を表しています。

図1. 世代間の参照
世代間の参照

カードマーキング

サンのJDKは、古い世代のオブジェクトのフィールドで保持されたポインターへの修正を識別するためにカードマーキングと呼ばれるアルゴリズムの最適化された形を使用しています。このアプローチでは、ヒープが、1セットのカード(その各々はメモリ・ページより通常小さい)に分割されます。JVMは、ヒープの中の各カードに対応する1ビット(あるいはいくつかの実行でのバイト)で、カードマップを維持します。ヒープ中のオブジェクト中のポインター・フィールドが修正されるごとに、そのカード用のカードマップ中の対応するビットがセットされます。ガーベジ・コレクション時に、古い世代内のカードに関連したマーク・ビットが検査され、汚れたカードはより若い世代への参照を含んでいるオブジェクトとしてスキャンされます。その後、マーク・ビットはクリアされます。カードマーキングにはいくつかの欠点があります-カードマップのためのスペース追加、各ポインターがストアされた場所でする追加作業およびガーベジ・コレクション時にする追加作業。カードマーキングのアルゴリズムは、初期化していないヒープのポインターストアあたり、少なくとも2つか3つほどの命令処理を加えることができ、minorコレクション時に、汚れたカード上のどんなオブジェクトもスキャニングすることが必然的に伴います。


JDK 1.4.1デフォルトコレクター

デフォルトで、1.4.1 JDKはヒープを、若い世代および古い世代の2つのセクションに分割します。(現実には、ロードされたクラスやメソッドオブジェクトのストアのために使用される、3つ目のセクション(パーマネント・スペース)があります。)若い世代はコピーコレクターを使用して、しばしばEdenと呼ばれる生成スペースおよび2つの残存セミスペースに分割されます。

古い世代はmark-compactコレクターを使用します。ある一定の回数コピーして、残存した後、オブジェクトは、若い世代から古い世代まで促進されます。minorコレクションは、Edenやもう一つの残存セミスペース中の残存オブジェクトをもう一つの残存スペースにコピーし、より古い世代に潜在的にいくつかのオブジェクトを促進するでしょう。主要なコレクションは若い世代、古い世代の両方を集めるでしょう。System.gc()メソッドは主要なコレクションが小さなコレクションよりもはるかに長くかかることができるので、いずれにしても、常に主要なコレクション(それはSystem.gc()を控え目に使用するべき理由のうちの1つである)を常に引き起こします。プログラムによってminorコレクションをトリガーする方法はありません。

他のコレクションオプション

デフォルトで使用されるコピーおよびmark-compactコレクターに加えて、1.4.1 JDKはさらに他の4つのガーベジ・コレクションのアルゴリズム(その各々は異なる目的に適合する)を含んでいます。JDK 1.4.1はインクリメンタル・コレクター(それはJDK 1.2からあります)、およびマルチプロセッサー・システム上のより効率的なコレクションのための3つの新しいコレクター--並列のコピーコレクター、並列のスカビンジングコレクターおよび並行mark-sweepコレクター--を含んでいます。これらの新しいコレクターは、マルチプロセッサー・システム上のスケーラビリティ・ボトルネックになっているガーベジ・コレクターの問題に取り組みます。図2は、コレクションオプションを選ぶべきかについてのいくつかのガイドラインを示します。

図2. 1.4.1ガーベジ・コレクション・オプション(Folgmann IT-Consultingのご好意)
1.4.1ガーベジ・コレクション・オプション(Folgmann IT-Consultingのご好意)

インクリメンタル・コレクション

インクリメンタル・コレクション・オプションは1.2からJDKの一部でした。インクリメンタル・コレクションは、万一の場合、near-realtime systemsのように、より短いコレクション停止が、どこで非常に重要かを望まなくして、処理能力を犠牲にしてガーベジ・コレクションの停止を減らします。

インクリメンタル・コレクション(theTrainalgorithm)のためにJDKによって使用されたアルゴリズムは、古い世代と若い世代との間のヒープの新しいセクションを作成します。これらのヒープセクションは、"trains," (その各々は"cars."のシーケンスに分割される)に分割されます。各carは別々に集めることができます。効果的に、train carは古い世代から若い世代への参照を追跡しなければいけないばかりではなく、より古いtrainsからより若いtrainsへの参照、より古いcarsからより若いcarsへの参照をも意味する独立した世代を構成します。これは、mutatorおよびガーベジ・コレクターのために重要な仕事を作成するが、はるかに短いコレクションの停止を許可します。

並列と並行コレクター

JDK 1.4.1中の新しいコレクターは、マルチプロセッサー・システム上のガーベジ・コレクションの問題を処理するように設計されています。ほとんどのガーベジ・コレクションのアルゴリズムがある期間の間止めるので、単一のスレッドガーベジ・コレクションはすばやくスケーラビリティボトルネックになることができます。ガーベジ・コレクターがユーザプログラムスレッドをサスペンドしている間、全てのプロセスではなく、1つのプロセスがアイドル状態となります。新しいコレクターのうちの2つは、コレクションの停止時間を減少することを目指しています。―― 並列コピーコレクターおよび並行mark-sweepコレクター。他方、並列scavengeコレクターは、大規模なヒープの上のより高い処理能力のために設計されています。

―XX:+UseParNewGCJVMオプションによって可能にされた並列のコピーコレクターは、CPU中の多くのスレッド間のガーベジ・コレクションのワークを分割する若い世代のコピーコレクターです。―XX:+UseConcMarkSweepGCオプションによって可能にされた並行mark-sweepコレクターは、初期のマーク過程(そして再びその後、短時間の再マーク過程のために)のために一時的に止めて、そして、ガーベジ・コレクター・スレッドがユーザ・プログラムと並行して実行している間、ユーザ・プログラムがレジュームするのを許可します。並列コピーコレクターおよび並行mark-sweepコレクターは基本的にデフォルトのコピーおよびmark-compactコレクターのコンカレントバージョンです。―XX:+UseParallelGCによって可能にされた並列のスカビンジングコレクターはマルチプロセッサー・システム上の非常に大規模な(ギガバイト、そしてより大きな)ヒープのために最適化された若い世代のコレクターです。

アルゴリズムの選択

選択する6つのアルゴリズムで、どれを使用するべきかとおそらく思っていることでしょう。図2は、単一スレッドと並行スレッドのコレクターの分割、低停止と高い処理能力コレクターを分割してガイダンスを提示しています。アプリケーションおよび開発環境を与えられて、適切なアルゴリズムを選択するのに十分かもしれません。多くのアプリケーションについては、デフォルトコレクターがちょうどよく働きます--したがって、あなたがパフォーマンスの問題を持っていなければ、より多くの複雑さを招くことは、意味はありません。しかしながら、あなたのアプリケーションがマルチプロセッサー・システム上でディプロイするか、非常に大規模なヒープを使用する場合、コレクターオプションの変更でパフォーマンスの上昇を得るかもしれません。


ガーベジ・コレクターのチューニング

JDK 1.4.1は、さらにガーベジ・コレクションのチューニングのために多数のオプションを含んでいます。これらのオプションを引っ張り、かつ、それらの結果を測定して、相当な時間を過ごすことができます。したがって、ガーベジ・コレクタをチューニングして試す前に、最初にあなたのアプリケーションが徹底的にプロファイルされており、最適化されていることを確かめることにより、チューニングする投資に対するよりよい収益をおそらく得るでしょう。

ガーベジ・コレクションのチューニングを始める最初の場所は冗長なGCの出力を検討することです。これは、あなたにガーベジ・コレクション・オペレーションの所要時間、タイミングおよび頻度についての情報を与えるでしょう。ガーベジ・コレクションのチューニングの最も単純な形式は単に最大のヒープサイズ(―Xmx)を拡張していることです。ヒープが成長するとともに、コピーコレクションはより効率的になります。したがって、ヒープの拡張によって、オブジェクト単位でコレクションの損失を減少させます。最大のヒープサイズを増加させることに加えて、さらに若い世代に割り当てられたスペースの割合を増加させる―XX:NewRatioオプションを使用することもできます。さらに、―Xmnオプションで若い世代のサイズを明示的に指定することができます。もっと詳細に記述されたガーベジ・コレクションのチューニングについてのアドバイスを提示する多くの記事に関しては、参考文献を参照してください。


要約

JVMが発展したように、デフォルトのガーベジ・コレクターはますますよくなりました。世代別コレクターはJDK1.2により採用され、その後、初期のJDKで使用されていたmark-sweep-compactコレクターより、さらに良いメモリ割当とコレクション・パフォーマンスが提示されています。1.4.1 JDKは、マルチプロセッサー・システムおよび非常に大規模なヒープに対する新しいマルチスレッドのコレクションオプションを加えることにより、さらにガーベジ・コレクションの有効性を改善します。

参考文献

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=218789
ArticleTitle=Javaの理論と実践: 1.4.1 JVM中のガーベジ・コレクション
publish-date=11252003