← 一覧へ
コンピューティング・組込み
#가비지컬렉션#메모리관리#세대별GC#STW#ZGC
最終更新 · 2026-10-12

ガベージコレクション(Garbage Collection)

1. 概要

A. 定義

ガベージコレクション(GC)とは、プログラムがもはや参照せず再利用できない(unreachable)ヒープメモリ上のオブジェクトを、ランタイムが自動的に識別して回収し、開発者が明示的にメモリを解放しなくてもよいようにする自動メモリ管理技法である。

GCが登場した根本的な理由は手動メモリ管理の危険性にある。C/C++のように開発者がmalloc/free、new/deleteで割り当て・解放を直接担う環境では、解放を忘れるとメモリリーク(memory leak)が積み重なって長時間稼働するサーバが徐々に死に、既に解放した領域を再び使うとダングリングポインタ(dangling pointer)・二重解放(double free)によって未定義動作とセキュリティ脆弱性(Use-After-Free)が発生する。こうした不具合は再現が難しくデバッグコストが莫大である。GCは「オブジェクトの寿命(lifetime)を追跡する責任」を人からランタイムへ移し、生産性と安定性を同時に高めるために考案された。

B. 登場の背景と必要性

GCの歴史は1959年、ジョン・マッカーシーがLISPを設計する際にリスト構造の循環参照を扱うために初めて導入したことに始まる。以降、Smalltalk・Java・C#・Go・JavaScript・Pythonなど現代の主流言語の大半がGCを内蔵し、今日ではサーバ・モバイル・Webアプリケーションの大多数がGCの上で動く。背景にはアプリケーション規模と寿命の爆発的増大がある。数百のライブラリと数百万のオブジェクトが絡む大型システムで、すべてのオブジェクトの所有権(ownership)を人が追跡するのは現実的に不可能に近く、数か月間無停止で回るサーバでは小さなリークさえ致命的だからである。

ただしGCは「無料」ではない。自動化の代償として実行時点で回収作業が割り込み、このときアプリケーションスレッドを止めるSTW(Stop-The-World)区間が発生し、遅延(latency)とスループット(throughput)に影響を与える。したがって現代GC技術の発展史は、「回収の正確性を保ちつつ停止時間をいかに減らし隠すか」という問いへの絶え間ない応答だと言える。応答性が重要な金融取引システムや対話型サービスでは数十msの停止さえSLA違反になり得るため、GCの選択とチューニングは技術士の観点で重要な設計上の意思決定となる。

さらにGCは単に「メモリを空ける機能」にとどまらず、アロケータ(allocator)・ランタイム・言語設計と緊密に噛み合った一つのサブシステムである。どの割り当て戦略を使うか(バンプポインタ対分離フリーリスト)、オブジェクト移動を許すか、ポインタにメタデータを埋め込むかによって、可能なGCアルゴリズムが変わる。ゆえにGCを正しく理解するとは、メモリ割り当て・オブジェクト配置・ランタイムバリアまで包含するメモリ管理全体の思考枠組みを備えることである。

C. ガベージコレクションの特徴

GCはいくつかの本質的特徴を持つ。第一に、到達可能性(reachability)に基づく。「ゴミ」の定義は「今後使わないオブジェクト」ではなく「ルートから参照経路で到達できないオブジェクト」であり、GCは未来を予測せず現在の参照グラフだけで安全に回収対象を判別する。第二に、近似的保守性であり、到達可能なオブジェクトは絶対に回収しないが(安全性)、到達不能なオブジェクトを常に即座に回収するわけではない(遅延を許容)。第三に、ランタイム介入性である。割り当て圧力が閾値を超えるとアプリケーションの流れの途中で非同期的に介入するため、性能プロファイルが不連続になる。第四に、世代・局所性の活用であり、大半のオブジェクトはすぐ死ぬという経験的仮説(弱い世代仮説)を積極的に利用する。

これらの特徴は、GCが「あればメモリを気にしなくてよい魔法」ではなく、明確なコストと動作モデルを持つ資源管理サブシステムであることを意味する。技術士の観点で重要なのはGCの有無ではなく、それがいつ・どれだけ・どのように介入してアプリケーションの遅延とスループットに影響するかを定量的に理解し制御する能力である。以下ではGCの作動原理から現代的な超低遅延コレクタまで、そしてこれを実務で運用・チューニングする観点を順に見ていく。

2. ガベージコレクションの基本原理と全体構造

GCの出発点はルート集合(root set)である。ルートとは、スレッドスタックのローカル変数、CPUレジスタ、グローバル・静的変数のように「プログラムが直接手に握っている参照」を指す。GCはこのルートから始めて参照を辿り到達可能なオブジェクトをすべて印付けし、印の付かない残りをゴミとみなす。下の構造図はヒープを参照グラフとして眺めた全体像である。

flowchart LR
    subgraph Roots["ルート集合(Root Set)"]
        S["スタックのローカル変数"]
        G["グローバル/静的変数"]
        R["レジスタ"]
    end
    S --> A["オブジェクトA(到達可能)"]
    G --> B["オブジェクトB(到達可能)"]
    A --> C["オブジェクトC(到達可能)"]
    B --> C
    D["オブジェクトD(ゴミ)"] --> E["オブジェクトE(ゴミ)"]
    E -.->|"循環参照"| D
    style D fill:#f8d7da
    style E fill:#f8d7da

ルート集合を正確に把握することは見た目より厄介である。コンパイラが各実行地点でどのスタックスロット・レジスタが実際にオブジェクト参照を保持しているかを知らせるスタックマップ(stack map)を提供してこそ、GCは「正確な(precise)」回収を行える。こうした情報がない環境(例:ネイティブスタックを保守的に走査する場合)では保守的(conservative)GCを使うが、ポインタのように見える整数値を参照と誤認して死んだオブジェクトを生かしてしまう不正確さが生じ得る。Java・C#のような管理ランタイムは正確なGCを、一部のC/C++向けGC(Boehm GC)は保守的方式を採用する。

上の図でDとEは互いを参照するがルートから到達できないため、ともにゴミである。この例は循環参照(cyclic reference)がなぜ重要かを示す。単に「自分を指す参照の数」だけを数える方式(参照カウント)ではDとEが互いを指して計数が0にならず永遠に回収されないが、到達可能性に基づく追跡方式はルートからの接続有無だけを見るため、この循環ゴミを正確に選り分ける。

GCアルゴリズムは大きく参照カウント方式と追跡型(tracing)方式に分かれる。両者は「ゴミをいつ・どう判定するか」で哲学が分かれる。

  • 参照カウント:参照が切れた瞬間に判定。遅延は平坦だが循環参照を捕まえられず常時更新コストがある。
  • 追跡型:定期的にルートからグラフを走査して判定。循環を正確に回収するが回収時に停止・負荷が集中する。

実務のランタイムは両者を混ぜて使う場合が多い。例えばCPythonは基本は参照カウントだが、循環ゴミのために追跡型の補助コレクタを併せて回す。次の手順図は最も代表的な追跡型であるマークスイープ(Mark-Sweep)の動作を段階的に示す。

flowchart TD
    Start["割り当て失敗/閾値到達"] --> STW["STW: アプリケーションスレッド停止"]
    STW --> Mark["Markフェーズ: ルートからグラフを走査し生存オブジェクトを印付け"]
    Mark --> Sweep["Sweepフェーズ: 未印付けオブジェクトをフリーリストへ回収"]
    Sweep --> Compact{"断片化が深刻?"}
    Compact -->|"はい"| Comp["Compact: 生存オブジェクトを片側へ圧縮"]
    Compact -->|"いいえ"| Resume["アプリケーション再開"]
    Comp --> Resume

Mark(印付け)フェーズではルートから深さ・幅優先でグラフを走査し、到達したオブジェクトごとにマークビットを立てる。この走査コストは生存オブジェクト数と参照密度に比例するため、生きているオブジェクトが多いヒープほどマークが高くつく。Sweep(掃除)フェーズではヒープ全体を走査してマークの付かないオブジェクトを空きメモリへ戻し、次の回収に備えてマークビットを初期化する。Sweepコストはヒープ全体の大きさに比例する。この方式は循環参照を正確に回収する利点があるが、回収後に空き領域が断片状に散らばる外部断片化(fragmentation)が蓄積する。そこでMark-Sweep-CompactはSweepの後に生存オブジェクトをメモリの片側へ寄せ、連続した空き領域を確保する。圧縮は断片化を無くし、以降の割り当てをポインタ増加だけで済ませるバンプポインタ(bump pointer)割り当てを可能にするが、オブジェクト移動に伴う参照更新コストがかかる。

ここで覚えておくべきは、GCの停止長が必ずしもヒープサイズに比例しないという事実である。初期のコレクタではヒープが大きくなるほど全面STW時間が長くなり、数百GBのヒープでは一度のFull GCが数秒に達してサービスが事実上停止する問題があった。現代の同時・領域ベースのコレクタはこの比例関係を断つことを核心目標とし、これこそが次の各節で見るアルゴリズム進化の根本的動機である。

3. 核心アルゴリズム — 参照カウント・コピー・世代別回収

A. 参照カウント(Reference Counting)

参照カウントは各オブジェクトが「自分を指す参照の数」をヘッダに持ち、参照が生じれば+1、切れれば−1し、0になった瞬間に即座に回収する方式である。別途のコレクタスレッドがヒープ全体を走査せず、参照代入という日常的な演算に回収ロジックを分散させる点で追跡型と哲学が根本的に異なる。CPythonとSwift(ARC, Automatic Reference Counting)が代表的である。

最大の利点は即時性と予測可能性である。ゴミが発生したまさにその地点で回収が起こるためメモリが速やかに返され、全面的なSTW停止が原理的に不要で遅延分布が平坦である。また回収コストがプログラム全般に細かく散らばっているため、コレクタがある瞬間にまとめて働く追跡型特有の「性能の崖」が緩和される。このため応答性が重要なモバイル(iOS/Swift)でARCが好まれる。

しかし参照カウントには構造的弱点が明瞭である。第一に、先に見たとおり循環参照を自力で回収できない。互いを指す二つのオブジェクトは計数が1を下回らず永遠に残るため、CPythonは定期的に回る循環コレクタ(cyclic collector)を、Swiftは開発者が明示するweak/unowned参照を補完策として置く。第二に、すべての参照代入ごとに計数を更新せねばならず常時オーバーヘッドが敷かれ、マルチスレッドではこの更新を原子的(atomic)に処理せねばならずキャッシュライン競合コストが大きくなる。実際、CPythonがGIL(グローバルインタープリタロック)を長く保ってきた背景の一つがまさにこの参照カウントのスレッド安全性問題であり、近年のfree-threaded CPythonの実験が難しい理由もここにある。第三に、大きなオブジェクトグラフの最後の参照が切れると連鎖的に多数の破棄が一度に起こり瞬間的な遅延スパイクが生じ得る。

B. コピー回収(Copying / Scavenging)

コピー回収はヒープをFromとToの二つの半空間(semi-space)に分け、回収時にFromの生存オブジェクトだけをToへコピー・圧縮した後、From全体を一度に空ける方式である。コピー過程で各生存オブジェクトの新しい位置を元の場所に転送ポインタ(forwarding pointer)として残し、同じオブジェクトを指す複数の参照がすべて一貫して新しいアドレスを指すよう更新する。

この方式の決定的な強みはコストが生存オブジェクト数のみに比例する点である。死んだオブジェクトは一切触らずFrom空間を丸ごと捨てるため、ゴミがいくら多くても回収時間が増えない。すなわち「ゴミが多いほど速い」特性を持ち、生存率の低い領域で圧倒的に有利である。またコピーと同時に圧縮が終わり外部断片化が原理的に無く、空き領域が常に連続しているため割り当てをポインタ増加だけで済ませるバンプポインタ方式が可能である。オブジェクトが割り当て順に集まり参照局所性(locality)が良くなる副次効果もある。

一方で利用可能メモリの半分しか使えないという空間の無駄が本質的弱点である。常に一方の半空間を空けておかねばならないからである。このためコピー回収をヒープ全体に適用する場合はまれで、生存率が低く利点の大きい「若いオブジェクト領域」だけに局所的に適用するのが一般的である。実際のJVMのYoung世代がEdenと二つのSurvivor空間に分かれ、Eden+片方のSurvivorの生存者をもう一方のSurvivorへコピーする構造こそ、このコピー回収の変形であり、半空間の無駄をSurvivor比率の調整で緩和した設計である。

C. 世代別回収(Generational GC)

世代別回収は「大半のオブジェクトは生成直後にすぐ死に、長く生き残ったオブジェクトは今後も長く生きる」という弱い世代仮説(weak generational hypothesis)に基づく。ヒープをYoung(Eden+Survivor)とOld(Tenured)世代に分け、割り当てはすべてEdenで起こる。下の表は世代別戦略の核心を整理したものである。

区分 Young世代(Minor GC) Old世代(Major/Full GC)
対象オブジェクト 生成されたばかりの短命オブジェクト 何度も生存して昇格した長寿オブジェクト
回収頻度 非常に頻繁 まれ
主アルゴリズム コピー回収 マークスイープコンパクト
回収コスト 短い(生存オブジェクト少) 長い(領域大、STW可能性)
効果 短い停止で大量回収 長寿オブジェクトのリーク・断片化整理

Young世代は生存率が低いためコピー回収で速やかに掃き出し、一定回数以上生き残ったオブジェクトはOldへ昇格(promotion)させる。核心的な難題は世代間参照である。OldオブジェクトがYoungオブジェクトを指すと、Youngだけを回収するときそのOldオブジェクトもルートのように扱わねばならない。このため書き込みバリア(write barrier)で世代間参照を記憶集合(Remembered Set)またはカードテーブル(card table)に記録しておき、Minor GC時にOld全体を走査せずとも参照を漏れなく辿る。この構造のおかげで世代別GCはヒープ全体を漁らずとも短い停止で大半のゴミを回収する。

実際の事例として、Webリクエスト処理サーバはリクエストごとに多数の一時的なDTO・文字列オブジェクトを作っては捨てるが、これらの大半は応答直後に死ぬ典型的な短命オブジェクトである。世代別GCはこのようなワークロードでEdenを数ms以内のMinor GCで空にし、計測上は割り当てられた全オブジェクトの90%以上が最初のMinor GCで回収される場合が多い。逆にセッション・キャッシュのように長く生きるオブジェクトはOldに集まるため、まれに起こるMajor GCがこれらを一度に整理する。このように寿命に応じてオブジェクトを分離し「頻繁・安価」と「たまに・高価」を組み合わせるのが世代別GCの設計思想である。

D. 三色表記と同時マークの整合性

同時(concurrent)マークを理解するには三色表記(tri-color marking)という抽象が必須である。すべてのオブジェクトを白(まだ未発見、回収候補)、灰(発見したが子参照未探索)、黒(子まで全て探索完了)に分類し、灰が尽きるまで「灰を黒に塗りその子を白から灰へ」前進させる。マークが終わると白のまま残ったオブジェクトがゴミである。

問題はマークとアプリケーションが同時に回るときに発生する。既に黒に塗ったオブジェクトが新たに白オブジェクトを参照するよう変わり、その白を指していた灰の経路が消えると、生きているオブジェクトをゴミと誤認して回収する致命的な誤りが生じる。これを防ぐためGCは書き込みバリア(write barrier)で参照変更を横取りして不変条件を守る。代表戦略が、変更される新しい参照を灰色化する増分更新(incremental update)と、切れる古い参照を保存するSATB(Snapshot-At-The-Beginning)であり、G1はSATBを採用する。この仕組みのおかげでZGC・Shenandoahのような同時コレクタがアプリケーションをほとんど止めずとも安全にマークを完成できる。

4. 停止時間を減らす現代GC — 同時・並列・領域ベース

初期GCが全面STWだったのに対し、現代GCはアプリケーションと同時に(concurrent)回収するか、作業を細かく刻んで停止を分散する。Javaエコシステムを中心に主要コレクタを比較すると次のとおりである。

コレクタ 戦略 目標停止 特徴
Serial GC 単一スレッドSTW 相対的に長い 小型ヒープ・単一CPU環境
Parallel GC 複数スレッドSTW 長い(スループット優先) バッチ・スループット最適化
CMS(deprecated) 同時マークスイープ 中程度 断片化・同時モード失敗の問題
G1 GC 領域(Region)ベース 予測型(数百ms→数十ms) ヒープをリージョンに分割、ゴミの多いリージョン優先
ZGC / Shenandoah 同時圧縮 1ms以下を志向 ロード/カラードポインタ、大型ヒープで超低遅延

G1(Garbage First)はヒープを同一サイズのリージョン(region)に細かく分け、各リージョンのゴミ比率を追跡して回収効率の高いリージョンから(garbage-first)回収することで、ユーザが指定した目標停止時間を予測的に守ろうとする。ZGC・Shenandoahは一歩進んで、オブジェクトを移動させる圧縮までアプリケーションと同時に行い、ヒープサイズと無関係に停止を1ms水準に抑える。これを可能にする核心技術がロードバリア(load barrier)と、ポインタに状態ビットを埋め込むカラードポインタ(colored pointer)であり、アプリケーションが移動中のオブジェクトを読むとき透過的に最新アドレスへ再指定(remapping)する。

同時・領域ベースのGCといえども失敗モードが存在することを運用者は知っておくべきである。同時回収が進む間もアプリケーションは割り当てを続けるため、回収速度が割り当て速度に追いつけなければOld空間が先に埋まる同時モード失敗(concurrent mode failure)や昇格失敗(promotion failure)が発生する。このときランタイムは最後の手段として全面STWのFull GCへ戻り、まさにこの瞬間が超低遅延サービスで最も長い停止として観測される。したがって同時GCチューニングの核心は「停止を0にする」ことではなく、コレクタが割り当て圧力に遅れないようヒープ余裕と回収開始閾値を調整することである。実務ではGCログからこの失敗イベントの頻度を核心指標とし、ヒープサイズ・コレクタパラメータを調整する。

ここで必ず押さえるべき概念がSTWとセーフポイント(safepoint)である。GCが参照グラフを一貫して見るにはすべてのスレッドが「ヒープ状態が整合する地点」で一時停止せねばならず、この地点がセーフポイントである。同時GCもマーク開始・終了など短い瞬間には全スレッドをセーフポイントに立たせるため、「STWが全く無い」ではなく「STWが非常に短くヒープサイズに比例しない」が正確な表現である。また長寿オブジェクトの参照を切らずに発生する論理的リーク(例:キャッシュ・リスナにオブジェクトが登録され続けGCが回収できない)はGCがあっても生じる問題で、GCは「到達不能」だけを回収し「不要」は判断できないという限界を明確に示す。

5. 深化 — 最新動向と実務適用事例

GC技術は超低遅延・大容量・観測可能性の軸で急速に進化している。Java陣営ではZGCが世代別(Generational ZGC, JDK 21で正式)に拡張され低遅延とスループットを同時に捉えようとし、Shenandoahも世代別モードを導入した。これは「同時GCはスループットの損」という長年の通念を弱い世代仮説と結合して緩和しようとする流れである。一方Go言語は低遅延を最優先に設計された同時マークスイープGCを使うが、圧縮を諦める代わりにサイズ別分離割り当て(size-classed allocator)で断片化を抑える。Goが圧縮を諦めたことは「すべての選択に代償がある」ことをよく示す。圧縮が無いので停止は短いが断片化・割り当て経路コストを甘受せねばならず、これをメモリアロケータ設計で埋める。.NETの場合はサーバGCとワークステーションGC、そしてバックグラウンドGCを状況に合わせて切り替えられるようにし、同一ランタイム内でもスループット・遅延目標に応じてGC挙動を変えられるよう設計されている。近年は割り当て率をもとに回収時点を調整するGC調整役(pacer)と、実際のハードウェアメモリ圧力に反応するソフトメモリリミット(soft memory limit)が導入され、クラウドコンテナのメモリ上限とGCを連動させる実務要求に対応している。

実務適用事例としてKubernetes環境のJVMコンテナを挙げられる。かつてJVMはコンテナのcgroupメモリ上限を認識できずホスト全体のメモリを基準にヒープを取りOOM Killを誘発したが、最新JVMはコンテナ認識(UseContainerSupport)でcgroup上限を読みヒープを比率(MaxRAMPercentage)で設定する。対話型APIゲートウェイや決済サーバのようにテール遅延(tail latency)が重要なサービスはG1からZGCへ切り替えP99停止を数百msから一桁msに下げた事例が多数報告される。逆に夜間バッチ・データパイプラインのように総スループットが核心ならParallel GCが依然有利である。このように「正解GC」は無く、ワークロードの遅延・スループット・メモリ特性に合わせた選択と計測ベースのチューニングが核心である。

GC運用の観測可能性も重要になった。GCログを収集して割り当て率、Minor/Major頻度、昇格率、停止分布(P99/P99.9)を監視し、昇格失敗・同時モード失敗・頻繁なFull GCを早期に検知することがSRE実務の基本になった。例えばあるサービスで応答遅延のテールが急に長くなったなら、アプリケーションのコード変更が無くても割り当て率の急増でMinor GCが過度に頻繁になったか、メモリリークでOldが埋まりFull GCが繰り返されるのが原因である場合が多い。このときGCログとヒープダンプ分析(例:支配ツリーdominator treeで大きなオブジェクト保有者を追跡)を組み合わせれば原因を正確に突き止められ、GCの可視性は性能事故対応の一次診断ツールとして使われる。

6. 考慮事項および示唆点

技術士の観点でGCは単なるメモリ自動化を超え、システム性能・コスト・安定性を左右するアーキテクチャ上の意思決定として扱わねばならない。

  • 遅延対スループットのトレードオフ:同時・領域ベースのGCは停止を減らすが書き込み/ロードバリアと追加スレッドでCPU・スループットを消費する。SLAの性格(平均スループットかテール遅延か)をまず定義してからコレクタを選択せねばならず、選択は必ず負荷試験とGCログ分析で検証する。
  • ヒープサイズとメモリコストの均衡:GCは余裕空間が多いほど頻度が減り効率的だが、メモリはクラウドコストに直結する。ヒープをやみくもに大きくするより割り当て率を下げる(オブジェクト再利用・プーリング・ストリーミング)設計が根本的処方であり、特にコンテナではcgroup上限とGCを必ず連動させる。
  • 論理的リークと設計責任:GCがあってもグローバルキャッシュ・イベントリスナ・ThreadLocalにオブジェクトが縛られ回収されないリークは発生する。明示的な寿命管理(弱い参照weak reference、明示的解放、キャッシュ期限切れポリシー)を設計に含めねばならず、GC依存がメモリ設計の免罪符にはならない。
  • 言語・ランタイム選択との連携:リアルタイム性が極度に重要な制御・高頻度トレーディング領域ではGC停止自体が許されず、Rustの所有権(ownership)ベースの無GCモデルやoff-heap・オブジェクト再利用技法が選ばれる。逆に生産性と安定性が優先されるエンタープライズバックエンドはGC言語が合理的である。GCは問題領域と非機能要求に合わせた言語・ランタイム戦略の一部として共に設計されねばならない。
  • セキュリティ観点の両面性:GCはUse-After-Free・二重解放のようなメモリ安全性脆弱性を構造的に除去し攻撃対象面を大きく減らす。一方でGC停止の発生パターンがサイドチャネルとして観測され得て、機微情報(暗号鍵など)が回収前までヒープに残存しメモリダンプで露出する危険がある。したがってセキュリティ機微データは使用直後に明示的に0で上書きするか、off-heapで管理する補完が必要である。
  • 展望:世代別同時GCの普及、コンテナ・NUMA・大容量ヒープ認識の強化、機械学習ベースのGCパラメータ自動チューニング、そして所有権モデル(Rust)と自動GCの共存が当面の核心的流れになるだろう。技術士はGCを「勝手にうまくいくもの」ではなく観測・計測・チューニングの対象として見る運用哲学を組織に定着させる必要がある。

参考資料


一言まとめ: ガベージコレクションは到達可能性を基準にゴミオブジェクトを自動回収するメモリ管理技法であり、参照カウント・追跡・コピー・世代別戦略からG1・ZGCのような同時・領域ベースの超低遅延コレクタへ発展してきた。遅延・スループット・メモリコストのトレードオフを計測ベースでチューニングすることが技術士的核心である。