ガベージコレクションアルゴリズムの内部(世代別・並行GC)
GCの停止時間がどこから来てどう削るのかが腑に落ちる解説。マーク・スイープからG1・ZGCの並行回収とwrite barrierまで、内部アルゴリズムを正確に押さえる。
- 1.GCの基本3操作はマーク(到達可能性の探索)・スイープ(回収)・コンパクション(圧縮)で、コピーGCはマークと圧縮を同時に行う方式。
- 2.世代仮説(多くのオブジェクトは短命)を前提に若い世代を頻繁・小規模に回収し、全体走査を避けて停止時間を抑える。
- 3.G1やZGCは並行マーク・並行コピーとwrite/load barrierで強い不変条件を保ち、停止時間をヒープ規模からほぼ切り離す。
到達可能性という大前提
GCが回収するのは「使われていない」オブジェクトではなく、ルート集合から到達できないオブジェクトです。ルートはスタック上のローカル変数、レジスタ、静的フィールド、JNIハンドルなどで、ここから参照グラフを辿れる範囲が「生存(live)」、辿れなければ「ごみ(garbage)」とみなされます。GCの正しさは「到達可能なオブジェクトを決して回収しない」という安全性(soundness)に依存し、逆に到達不能を即座に回収するかどうかは方式により異なります。
基礎はガベージコレクションで扱ったので、本稿はトレース型GCの内部アルゴリズムと停止時間制御に踏み込みます。
マーク・スイープ・コンパクション・コピー
トレース型GCの基本操作は次の通りです。マークはルートからのグラフ探索(DFS/BFS)で生存オブジェクトに印を付けます。スイープは印の付かない領域を空き領域として回収します。
| 方式 | 仕組み | 長所 | 短所 |
|---|---|---|---|
| マークスイープ | 印付け→未印を空きリストへ | 移動なしで実装が単純 | 断片化が進む / 確保が遅くなる |
| マークコンパクト | 印付け→生存を片側へ詰める | 断片化を解消 | オブジェクト移動と参照書換のコスト |
| コピーGC | 生存を別空間(To)へコピー | 確保がポインタ加算のみで高速 | ヒープを半分しか使えない |
コピーGC(Cheneyのアルゴリズム)は、From空間の生存オブジェクトをTo空間へ移すと同時に詰めるため、マークとコンパクションを一度に行えます。確保(allocation)はバンプポインタ(空き先頭を加算するだけ)で済み、断片化も起きません。代償として常にヒープの半分が予備になりますが、生存率が低い領域なら「生きている分だけ」コピーすればよく、死んだオブジェクトのコストはゼロです。この性質が次の世代別GCと噛み合います。
世代仮説と世代別GC
経験則として「ほとんどのオブジェクトは生成直後に死ぬ(weak generational hypothesis)」が成り立ちます。そこでヒープを若い世代(young)と古い世代(old)に分け、若い世代だけを頻繁に回収するマイナーGCを行います。若い世代は生存率が低いためコピーGCが最適で、HotSpotではEden+2つのSurvivor領域で構成し、一定回数生き延びたオブジェクトをoldへ**昇格(promotion)**させます。
若い世代だけを回収するには、old→youngの参照もルートとして扱う必要があります。これを全old走査で探すと世代別の利点が消えるため、カードテーブル(ヒープを512バイト等のカードに区切り、old側に書き込みが起きたカードへ印を付ける)やremembered setで該当領域だけを記録・走査します。この記録を維持するのが後述のwrite barrierです。
世代別GCの効果は計算量の観点でも明確です。回収コストは概ね生存オブジェクト数に比例し、ヒープ全体ではなく若い世代の生存分だけを走査するため、停止時間が小さく保たれます(計算量と Big-O 記法)。
write barrier — 並行・世代別を支える仕組み
write barrierは、オブジェクトの参照フィールドへの書き込み(a.f = b)の前後に処理系が差し込む短いコードです。用途は2つあります。1つはカードテーブルの更新(世代別)、もう1つは並行マーク中の不変条件維持です。
// 概念的な write barrier(参照書込みをフック)
write(obj, field, newRef) {
pre_barrier(obj.field); // 旧値を退避(SATB系)
obj.field = newRef; // 実際の書き込み
post_barrier(obj, newRef);// カード印付け / 色の伝播
}
並行マークの難しさは、マーカーがグラフを辿っている最中にアプリ(mutator)が参照を書き換えると、生存オブジェクトを見落としかねない点です。マーク状態を白(未到達)・灰(到達済だが子未走査)・黒(子まで走査済)の3色で表すと、危険なのは「黒→白の参照が新規に作られ、その白へ向かう灰の経路が消えた」場合です。これを防ぐ代表的な不変条件が2つあります。
- SATB(snapshot-at-the-beginning): マーク開始時点の生存グラフを保証する。上書きで消える旧参照をpre-barrierで灰に積む。G1が採用。
- incremental update: 黒オブジェクトに白参照が入ったらpost-barrierで黒を灰に戻す。
停止時間制御 — G1とZGC
ここまでの部品で「いかに止めずに回収するか」が設計できます。マーク・スイープ自体を非同期的にアプリと並走させ、避けられないstop-the-world(STW)を極小化するのが現代GCの核心です。
G1(Garbage-First)はヒープを多数の固定サイズリージョンに分割し、世代を物理的に固定しません。並行マークでリージョンごとの生存量を見積もり、「ごみが多いリージョンを優先(garbage-first)」して回収します。コピー(evacuation)自体はSTWで行いますが、一時停止時間の**目標値(pause target)**を設定でき、その予算内に収まるリージョン数だけを選んで回収することで停止時間を制御します。
ZGCは64ビットポインタの未使用ビットにメタデータ(色)を埋め、参照の読み出し時にload barrierで「このオブジェクトは移動済みか」を判定します。移動済みなら新アドレスへ即座に張り替える(self-healing)ため、コピーすらアプリと並行に行えます。結果としてSTWはルートスキャン等のごく短い区間に限られ、停止時間がヒープサイズにほぼ依存しません(公称ミリ秒未満〜数ミリ秒)。
| GC | 停止モデル | ヒープ構造 | バリア |
|---|---|---|---|
| Parallel | 並列STW(全体) | 世代別・連続 | なし(write barrierのみ) |
| G1 | 並行マーク+STWコピー | リージョン | SATB write barrier |
| ZGC / Shenandoah | ほぼ全並行(マーク+コピー) | リージョン | load / 参照バリア |
トレードオフを正しく読む
並行GCはタダではありません。バリアは全参照アクセスに小さなオーバーヘッドを課し、マーカーとアプリが同じCPUとメモリ帯域を奪い合うためスループットは低下しがちです。さらに、アプリが並走する以上、回収が確保に追いつかないallocation stall(確保の足止め)や、最悪STWへ退避する場面も残ります。
バッチ処理のように総処理時間(スループット)最優先ならParallelGCが有利で、低レイテンシ要求が強いオンライン系ではG1やZGCが向きます。ヒープが数十GB超で停止が許されないならZGC/Shenandoahが第一候補です。「最新のGCが常に最良」ではなく、スループット・停止時間・フットプリントの三角形のどこを取るかという問題です。
まとめ
トレース型GCはマーク・スイープ・コンパクション・コピーを部品とし、世代仮説によって「若い世代を小さく頻繁に」回収して停止時間を抑えます。世代を跨ぐ参照と並行マークの正しさはwrite barrierと3色不変条件(SATB / incremental update)が支え、G1はリージョン単位の予算制御で、ZGC/Shenandoahは色付きポインタとload barrierでコピーまで並行化して、停止時間をヒープ規模から切り離します。GC選択はスループット・停止時間・フットプリントのトレードオフであり、バリアコストや並行マークの仕組みまで理解して初めて、停止時間の正体と削り方が見えてきます。
プログラミング Article
ガベージコレクションアルゴリズムの内部(世代別・並行GC)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
ガベージコレクション
比較で見る軸
難易度: advanced / カテゴリ: プログラミング / タグ数: 5
導入後に効く点
世代仮説(多くのオブジェクトは短命)を前提に若い世代を頻繁・小規模に回収し、全体走査を避けて停止時間を抑える。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- プログラミング
- タグ数
- 5
判断チェックリスト
- 自社の用途が「ガベージコレクション / メモリ管理」に近いか確認する。
- 強みである「GCの基本3操作はマーク(到達可能性の探索)・スイープ(回収)・コンパクション(圧縮)で、コピーGCはマークと圧縮を同時に行う方式。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。