Java プラットフォームのガーベッジ コレクション機構は、開発者の生産性を大幅に向上させますが、ガーベッジ コレクションの実装が不十分だと、アプリケーション リソースを過度に消費してしまう可能性があります。 JVM パフォーマンス最適化シリーズのこの第 3 回の記事では、Eva Andreasson が Java プラットフォームのメモリ モデルと GC メカニズムの概要を Java 初心者に説明します。
ガーベッジ コレクション (GC) は、到達可能な任意の Java オブジェクトによって参照されなくなった占有メモリを解放することを目的としたプロセスであり、Java 仮想マシン (JVM) の動的メモリ管理システムの重要な部分です。 典型的なガベージコレクションサイクルでは、まだ参照されている、つまり到達可能なすべてのオブジェクトが保持されます。
ガベージ コレクションとさまざまな GC アプローチおよびアルゴリズムを理解するためには、まず Java プラットフォームのメモリ モデルについていくつかのことを知る必要があります。 シリーズを読む
- Part 1: 概要
- Part 2: コンパイラ
- Part 3: ガーベッジコレクション
- Part 4: 同時コンパクト化GC
- Part 5: スケーラビリティ
ガーベジコレクションとJavaプラットフォームのメモリモデル
Javaアプリケーションのコマンドラインで起動オプション-Xmx
を指定すると(例:java -Xmx:2g MyApp
)メモリはJavaプロセスに割り当てられます。 このメモリはJavaヒープ (または単にヒープ) と呼ばれる。 これは、Javaプログラム(またはJVM)によって作成されるすべてのオブジェクトが割り当てられる専用のメモリアドレス空間です。
最終的には、Java ヒープがいっぱいになります。これは、割り当てを行うスレッドが、割り当てたいオブジェクトのための空きメモリの十分な大きさの連続したセクションを見つけることができないことを意味します。 その時点で、JVMはガベージコレクションが必要だと判断し、ガベージコレクタに通知する。 ガベージコレクションは、JavaプログラムがSystem.gc()
を呼び出したときにも引き起こされることがあります。 System.gc()
を使用しても、ガベージコレクションが保証されるわけではありません。 ガベージコレクションが開始される前に、GCメカニズムはまずそれを開始しても安全かどうかを判断します。 例えば、進行中のオブジェクト割り当ての途中や、最適化された CPU 命令のシーケンスを実行している途中でガベージコレクションを開始するのは良くないと簡単に説明しました (コンパイラーに関する以前の記事を参照)。 ガベージコレクタはまた、死んだオブジェクトを直ちに収集することを要求されない。 死んだオブジェクトは、その後のガベージコレクションサイクル中に最終的に収集される。 ガベージコレクションを実装する方法はたくさんありますが、この2つの仮定はすべての種類に当てはまります。 ガベージコレクションの本当の課題は、生きている(まだ参照されている)ものをすべて識別し、参照されていないメモリをすべて再生することですが、実行中のアプリケーションに必要以上の影響を与えることなく行うことです。 ガベージ コレクションには、次の 2 つの義務があります。
- アプリケーションの割り当て率を満たすために、参照されないメモリをすばやく解放して、メモリ不足にならないようにする。
Two kinds of garbage collection
このシリーズの最初の記事で、参照カウントとトレース コレクターというガベージ コレクションの 2 つの主要なアプローチに触れました。 今回は、それぞれのアプローチをさらに掘り下げ、実環境でトレース コレクターを実装するために使用されるアルゴリズムのいくつかを紹介します。
JVM パフォーマンス最適化シリーズを読む
- JVM パフォーマンス最適化、パート 1: 概要
- JVM パフォーマンス最適化、パート 2: コンパイラ
参照カウント コレクター
参照カウント コレクターは、各 Java オブジェクトを指す参照数を記録します。 オブジェクトのカウントがゼロになると、そのメモリはすぐに再利用できるようになる。 この再生されたメモリへの即時アクセスは、ガベージ コレクションに対する参照カウント方式の主な利点です。 参照されないメモリを保持するためのオーバーヘッドは、ほとんどありません。 しかし、すべての参照カウントを最新に保つことは、かなりコストがかかります。
参照カウント コレクターの主な難点は、参照カウントを正確に保つことです。 もう 1 つのよく知られた課題は、循環構造の処理に関連する複雑さです。 2 つのオブジェクトが互いに参照し、生きているオブジェクトがそれらを参照しない場合、それらのメモリは決して解放されません。 両方のオブジェクトは、永遠にゼロでないカウントを持ったままです。
Tracing collectors
Tracing collectors は、すべてのライブオブジェクトはライブオブジェクトであることが知られている初期セットからのすべての参照とその後の参照を繰り返しトレースすることによって見つけることができるという仮定に基づいている。 ライブオブジェクトの初期セット (ルートオブジェクトまたは略して単にルートと呼ばれる) は、ガベージコレクションがトリガされた瞬間に、レジスタ、グローバルフィールド、スタックフレームを分析することによって位置が特定されます。 最初のライブオブジェクトが特定された後、トレースコレクターはこれらのオブジェクトからの参照を追跡し、ライブとしてマークされるようにキューに入れ、その後、その参照をトレースするようにします。 見つかったすべての参照オブジェクトをライブにマークすることは、時間の経過とともに既知のライブセットが増加することを意味します。 このプロセスは、すべての参照される(したがってすべてのライブ)オブジェクトが見つかり、マークされるまで続けられます。 トレースコレクターがすべてのライブオブジェクトを見つけたら、残りのメモリを再要求します。
トレースコレクターは、参照カウントコレクターとは異なり、循環構造を扱うことができます。 ほとんどのトレース コレクターの欠点はマーキング フェーズで、参照されないメモリを再要求できるようになるまでに待ち時間が発生します。
トレース コレクターは動的言語でのメモリ管理に最もよく使用されます。 この記事の残りの部分では、ガベージ コレクションへのこのアプローチを実装するいくつかのアルゴリズムから始めて、トレース コレクターに焦点を当てます。
トレース コレクター アルゴリズム
コピーとマーク アンド スイープ ガベージ コレクションは新しいものではありませんが、これらは今日でもトレース ガベージ コレクションを実装する最も一般的な 2 つのアルゴリズムとなっています。
コピー コレクター
従来のコピー コレクターは、From-space と To-space — つまり、ヒープの 2 つの別々に定義されたアドレス空間 — を使用します。 ガベージコレクションの時点で、from-space として定義された領域内の生きているオブジェクトは to-space として定義された領域内の次に利用可能なスペースにコピーされる。 from-space内のすべてのライブオブジェクトが移動されると、from-space全体が回収されることができる。 このアルゴリズムの古い実装では、from-space と to-space は場所を交換します。つまり、to-space がいっぱいになると、ガベージ コレクションが再びトリガーされ、図 1 で示すように to-space が from-space になります。 従来のコピー ガベージ コレクション シーケンス (クリックで拡大)
コピー アルゴリズムのより現代的な実装では、ヒープ内の任意のアドレス空間を to-space および from-space として割り当てることが可能です。 これらの場合、それらは必ずしも互いに場所を交換する必要はなく、むしろそれぞれがヒープ内の別のアドレス空間になる。
コピー・コレクターの 1 つの利点は、オブジェクトが to-space で緊密に一緒に割り当てられ、断片化を完全に排除することである。 断片化は、他のガベージ コレクション アルゴリズムが苦労している一般的な問題です。 stop-the-world 実装では、コピーする必要のある領域が大きいほど、アプリケーション パフォーマンスへの影響が高くなります。 これは、応答時間に敏感なアプリケーションにとっては不利な点です。 コピー・コレクターでは、すべてがfrom-spaceで生きているときの最悪のシナリオも考慮する必要があります。 つまり、to-space は from-space のすべてのオブジェクトをホストするのに十分な大きさでなければなりません。 コピー アルゴリズムは、この制約によりわずかにメモリ非効率です。
マーク アンド スイープ コレクター
企業の生産環境に展開されるほとんどの商用 JVM は、マーク アンド スイープ (またはマーキング) コレクターを実行し、これはコピー コレクターが行うパフォーマンスへの影響を与えません。 最も有名なマーキング コレクターは CMS、G1、GenPar、および DeterministicGC (リソースを参照) です。
マーク アンド スイープ コレクターは参照を追跡し、見つかったオブジェクトに “live” ビットでマークします。 通常、設定されたビットはヒープ上のアドレスまたは場合によってはアドレスのセットに対応する。 ライブビットは、例えば、オブジェクトヘッダのビット、ビットベクトル、またはビットマップとして格納されます。
すべてがライブマークされた後、掃引段階が開始されます。 コレクターに掃引段階がある場合、基本的にヒープを再度 (ライブ セットだけでなくヒープ長全体) トラバースして、連続したメモリ アドレス空間のマークされていないチャンクをすべて見つけるための何らかのメカニズムが含まれます。 マークされていないメモリは自由であり、再利用可能です。 コレクターは次に、これらのマークされていないチャンクを結びつけて、組織化された空きリストにします。 ガベージコレクタには様々な空きリストがあり、通常はチャンクサイズによって整理されます。 いくつかの JVM (JRockit Real Time など) は、アプリケーション プロファイリング データとオブジェクト サイズ統計に基づいてリストを動的にサイズ調整するヒューリスティックなコレクターを実装しています。
掃引段階が完了すると、割り当てが再び開始されます。 新しい割り当て領域は空きリストから割り当てられ、メモリ チャンクはオブジェクト サイズ、スレッド ID ごとのオブジェクト サイズ平均、またはアプリケーションで調整された TLAB サイズに一致させることができます。
TLAB サイズの詳細
TLAB および TLA (Thread Local Allocation Buffer または Thread Local Area) パーティショニングは、JVM パフォーマンス最適化、パート 1 で説明します。
Downsides of mark-and-sweep collectors
マーク段階はヒープ上のライブ データ量に依存し、掃引段階はヒープ サイズに依存する。 マーク フェーズと掃引フェーズの両方が完了するまで、メモリを再利用するために待機する必要があるので、このアルゴリズムは、より大きなヒープおよびより大きなライブ データ セットのための一時停止時間の問題を引き起こします。 チューニングは、多くの場合、これらの段階のいずれかがアプリケーションまたはサービス レベル アグリーメント (SLA) のリスクになることを少なくとも延期するのに役立ちます。 (SLAは、アプリケーションが特定のアプリケーション応答時間、すなわちレイテンシーを満たすことを指定するものです)。 しかし、負荷の変化やアプリケーションの修正ごとにチューニングすることは、特定のワークロードと割り当て率に対してのみ有効であるため、繰り返しの作業となります。
マーク アンド スイープの実装
マーク アンド スイープ コレクションを実装するための商用で実証済みのアプローチは少なくとも 2 つあります。 1 つは並列アプローチ、もう 1 つは並列 (またはほとんど並列) アプローチです。
並列コレクター
並列収集とは、プロセスに割り当てられたリソースが、ガベージ コレクションのために並行して使用されることを意味します。 商業的に実装されたほとんどの並列コレクターは、モノリシックなストップ・ザ・ワールド コレクターで、ガベージ コレクション サイクル全体が完了するまで、すべてのアプリケーション スレッドが停止します。 すべてのスレッドを停止することで、マークとスイープフェーズを通じてガベージコレクションを終了するために、すべてのリソースを効率的に並列に使用することができます。 これは非常に高いレベルの効率につながり、通常SPECjbbのようなスループット・ベンチマークで高いスコアを得ることができます。 アプリケーションにスループットが不可欠な場合、並列アプローチは優れた選択肢となります
。