基礎●●●○○

ガベージコレクション

ガベージコレクション

不要になったヒープ領域を自動で回収する仕組み。メモリ安全と停止時間のトレードオフ。

概要

ガベージコレクション(GC)は、プログラムが確保したメモリのうち「もう誰からも使われなくなった領域」を自動で見つけ出し、回収して再利用可能にする仕組みです。プログラムは動作中に、オブジェクトや配列といったデータをヒープと呼ばれる領域に次々と確保します。使い終わった領域を放置すればメモリはいずれ尽きるため、「いつ・誰が・どうやって返却するか」はプログラミングの根源的な問題です。GC はこの返却作業をランタイム(言語の実行環境)に肩代わりさせる、という答えです。

Java・C#・Python・Go・JavaScript など、現代の主要な言語の多くは GC を標準で備えています。開発者は freedelete のような明示的な解放を書かず、「参照が届かなくなったものはいずれ回収される」と信じてロジックに集中できます。一方で、回収のためにプログラムを一時停止させる「stop-the-world」や、回収タイミングを制御しにくいことによるレイテンシの揺らぎなど、GC 固有のコストと付き合う必要も生まれます。

なぜ生まれたか

C 言語のように手動でメモリを管理する世界では、確保した領域は必ず自分で解放しなければなりません。ここには事故のパターンが3つあります。解放し忘れればメモリリーク(使わない領域が積み上がりメモリを食い尽くす)、同じ領域を二度解放すれば二重解放(ヒープ管理構造が壊れてクラッシュや脆弱性の温床になる)、解放済みの領域を指すポインタを使い続ければダングリングポインタ(別のデータを上書きし、再現困難なバグやセキュリティホールを生む)です。厄介なのは、これらのバグが「解放を書いた場所」ではなく、遠く離れた無関係な場所で発症することです。大規模なプログラムでは「この領域をもう誰も使っていない」ことを人間が確実に判断すること自体が難しく、メモリ管理はバグの最大の発生源のひとつでした。

GC の発想は「人間に判断させるから間違える。ならば、参照をたどれば機械的に判定できるようにしよう」というものです。起源は古く、1959年、LISP のために John McCarthy が考案しました。リスト構造を大量に生成・破棄する LISP では手動管理が非現実的だったためです。以後半世紀をかけてアルゴリズムが洗練され、1995年の Java の普及によって「GC があるのが普通」という感覚が業務開発の主流になりました。

詳細

生きているかどうかは「届くかどうか」で決める

GC の判定基準は「今後使われる可能性があるか」ではなく、「ルートから参照をたどって届くか(到達可能か)」です。ルートとは、スタック上のローカル変数、グローバル変数、CPU レジスタなど、プログラムが直接触れる起点のことです。ルートから参照の鎖をたどって届くオブジェクトは「生きている」、どこからも届かないものは「ゴミ」とみなして回収します。この単純明快な基準こそが、人間の曖昧な判断を機械的な処理に置き換えた核心です。

参照カウントとその弱点 — 循環参照

最も直感的な方式が参照カウントです。各オブジェクトに「自分を指している参照の数」を持たせ、参照が増えたら +1、減ったら -1 し、0 になった瞬間に即座に回収します。回収コストが分散されて停止時間が短く、Python や Swift が採用しています。しかし致命的な弱点があります。A が B を指し、B が A を指し返す「循環参照」では、外部から誰も届かなくなってもカウントは 1 のまま残り、永遠に回収されません。このため Python は循環参照を検出する補助 GC を別途備えています。

マークアンドスイープと世代別GC

もうひとつの主流がマークアンドスイープです。ルートから参照を再帰的にたどって生きているオブジェクトすべてに印を付け(マーク)、印のないものをまとめて回収します(スイープ)。参照の鎖を実際にたどるので、循環参照も正しくゴミと判定できます。派生として、生きているオブジェクトを別領域に詰め直して断片化を解消する「コピーGC」「コンパクション」もよく使われます。

次の部品では、実際に参照を切ってから GC を実行し、マーク(到達可能なものに印)とスイープ(印のないものを回収)の2段階が動く様子を確かめられます — 循環参照ペア E・F への参照を切って回収されるかを試してみてください。

⚡ 体験: 参照を切って、マーク&スイープを走らせる
ヒープ使用量536 / 800 B(累計解放 0 B)
クリックで参照を切断クリックで参照を切断クリックで参照を切断クリックで参照を切断クリックで参照を切断クリックで参照を切断クリックで参照を切断クリックで参照を切断ルートグローバル / スタックA64 BB128 BC32 BD96 BE48 BF48 BG80 BH40 B

金 = マーク済み(到達可能) / 朱 = スイープ対象 / 点線 = 切断された参照(クリックで再接続)

待機中

矢印(参照)をクリックして切断し、「GCを実行」でマーク&スイープの2段階を観察してみてください。G・H は最初からルートに届かない孤立ゴミ、E・F は互いを参照し合う循環ペアです。

実用的な GC の多くは、さらに「世代別GC」という最適化を重ねています。根拠は「世代仮説」— ほとんどのオブジェクトは生成直後に死ぬ、という経験則です。リクエスト処理の一時オブジェクトを思い浮かべると分かる通り、生まれたてのオブジェクトの大半は一瞬で不要になり、逆にしばらく生き延びたもの(設定、キャッシュなど)は長生きする傾向があります。そこでヒープを「新世代」と「旧世代」に分け、新世代だけを高頻度・高速に回収し(マイナーGC)、そこを生き延びたオブジェクトを旧世代へ昇格させ、ヒープ全体の回収(メジャーGC / フルGC)は低頻度に抑えます。

ヒープ新世代高頻度・高速に回収するマイナーGC生成直後に死ぬ大多数生存世代仮説: ほとんどのオブジェクトは若くして死ぬ旧世代低頻度に回収するメジャーGC設定・キャッシュなど長生きするオブジェクト数回のGCを生き延びたら昇格マイナーGCで即回収・領域を再利用
世代別GCのヒープ構成 — 大半のオブジェクトは新世代で死に、生き残りだけが旧世代へ昇格する

stop-the-world とレイテンシ

GC の代表的なコストが stop-the-world、つまり回収作業のためにアプリケーションの全スレッドを一時停止させることです。マーク中にオブジェクトの参照関係が書き換わると判定が壊れるため、素朴な実装では世界を止めるしかありません。バッチ処理なら停止も気になりませんが、Web サービスでは数百ミリ秒の停止がそのままレスポンスの遅延スパイクになります。そこで現代の GC は、アプリケーションと並行してマークを進める並行GC・インクリメンタルGC を発達させてきました。Go の GC や Java の ZGC は停止時間をミリ秒以下に抑える設計で、「スループット(総処理量)を取るか、レイテンシ(停止の短さ)を取るか」は GC チューニングの中心的なトレードオフです。

GC があってもメモリリークは起きる

見落とされがちですが、GC はメモリリークを根絶しません。GC が回収できるのは「届かなくなった」ものだけであり、「もう使わないのに参照が残っている」ものは生きていると判定され続けるからです。典型例は、グローバルなキャッシュや Map に登録したまま消し忘れたオブジェクト、解除し忘れたイベントリスナー、JavaScript のクロージャが意図せず大きなデータを掴み続けるケースです。GC 言語のメモリリーク調査では、ヒープダンプを取って「何が・どこからの参照で生き残っているか」をたどるのが定石です。

別解 — Rust の所有権

GC のコストを避けつつ手動管理の事故も防ぐ第三の道として、Rust の所有権システムがあります。「各値には所有者がただ1つ存在し、所有者がスコープを抜けたら解放される」というルールをコンパイル時に静的検査することで、実行時の GC なしにメモリ安全を保証します。解放タイミングがコードから決定的に読め、停止時間もゼロですが、その代わり所有権と借用のルールを開発者が学び、コンパイラと交渉しながら書く必要があります。「実行時に機械が回収する(GC)」「コンパイル時に機械が検証する(所有権)」「人間が管理する(手動)」という3つの選択肢の並びで捉えると、GC の位置付けがはっきり見えてきます。