最終更新:

ガベージコレクションの仕組み — 参照が残ると、なぜ片付かない?


カートを空にしても、控えからたどれる

控えが残っているcart:nullreceiptりんごのデータまだ、たどれる「不要なつもり」では回収されない両方の参照を外したcart:nullreceipt:nullりんごのデータこの例からたどれない回収の時刻は、これだけでは分からない
矢印はデータへの参照です。右ではこのコード内の参照を外しています。実際の回収には、ほかの参照とランタイムの動作も関わります。
ひよこ ひよこ
買い物カートを空にしたら、商品データも消えるの?
ペンギン先生 ペンギン先生
ほかに参照が残っているなら、まだたどれるよ。下の例ではカートと控えが同じ商品を指す。GCは気持ちの上で不要かではなく、参照をたどれるかなど、実装の仕組みで回収するんだ。
ひよこ ひよこ
GCは、メモリの片付け係なの?
ペンギン先生 ペンギン先生
そう考えると入りやすいね。使える起点からたどれなくなったオブジェクト等のメモリを回収する。JavaScriptのように自動管理する言語でも、参照を長く残しすぎると使用量が増え得るんだ。
ひよこ ひよこ
マーク&スイープはどう片付けるの?
ペンギン先生 ペンギン先生
起点からたどれるものに印を付け、たどれないものを回収対象にする考え方だよ。図の矢印はデータの参照。すべての起点からたどれないことが重要で、ある変数をnullにしただけで全部が消えるとは限らないよ。
ひよこ ひよこ
参照カウントも同じやり方?
ペンギン先生 ペンギン先生
参照の数を数える方式で、追跡して印を付ける方法とは違うよ。CPythonでは参照カウントと循環参照を扱うGCを併用する。Pythonのすべての実装が同じ回収時期を保証するわけではないんだ。
ひよこ ひよこ
循環参照って、互いに指していること?
ペンギン先生 ペンギン先生
そう。単純な参照カウントだけだと、外からたどれなくても数が0にならない場合がある。一方、到達可能性を調べる方式では、その輪にも外の起点から届かないかを判断できるんだ。
ひよこ ひよこ
若いデータを先に片付ける方法もある?
ペンギン先生 ペンギン先生
世代別GCだね。短命なオブジェクトが多いという傾向を利用し、全体を毎回調べる負担を減らす。世代の分け方や採用の有無は実装次第だから、どのGCもYoungとOldの2つに同じ形で分けるわけではないよ。
ひよこ ひよこ
片付けている間、アプリは止まる?
ペンギン先生 ペンギン先生
一時停止する処理や、アプリと並行して進める処理がある。Goの標準ツールチェーンのGCも並行処理を使うけれど、停止やCPU負担がゼロになる保証はない。実際の停止時間とメモリの推移を測って判断しよう。
ひよこ ひよこ
ファイルもGCへ任せていい?
ペンギン先生 ペンギン先生
ファイルや通信などは、言語やライブラリの手順で明示的に閉じよう。メモリの回収と外部リソースの終了は別だよ。下の例は参照の違いを見せるもので、GCが実行された時刻やRAMが減った量を測る実験ではないんだ。

まずは、カートを空にしても残る「控え」を見る

GCはメモリの回収を自動化する仕組みです。ただし、「もう使わないつもり」と「どこからもたどれない」は違います。

Node.jsが使えるなら、次をgc-demo.jsへ保存し、node gc-demo.jsで試します。ブラウザーのコンソールでも実行できます。

let cart = { name: "りんご", price: 120 };
let receipt = cart;
cart = null;
console.log(receipt.name);
receipt = null;
console.log(cart, receipt);
りんご
null null

cartをnullにしてもreceiptから商品を読めます。receiptもnullにした後、この小さなコード内からはその商品をたどれなくなります。実際のアプリなら、キャッシュ、イベント処理、別の配列などに参照が残っていないかも確認します。

この出力だけでGCの実行時刻は分かりません。デバッガーが参照を残す場合や、回収後もランタイムが確保済みの領域を持つ場合もあるので、RAMがすぐ減る保証とは分けましょう。

「たどれるか」と「参照の数」は別の方式

マーク&スイープは、起点からたどれるものを調べ、到達できないものを回収対象にする考え方です。単純な参照カウントは、参照数が0になることを使います。

互いに指し合うだけの輪でも、数だけを見れば0にならないことがあります。追跡する方式では、外の起点からその輪へ届くかを判断できます。CPythonは参照カウントに加え、循環参照を扱うGCを使います。

Cのmalloc/freeの手動管理、C++のRAII、Rustの所有権も異なる仕組みです。「GCがないから実行時の解放処理もない」という意味ではありません。

もう少し詳しく:停止・世代・使いすぎ

GCにはCPUやメモリの負担があり、アプリを一時停止する段階を持つ実装もあります。並行処理で負担を分散しても、すべての停止が消えるわけではありません。世代別の方式は短命なデータが多い傾向を使いますが、すべてのGCが採用する一律の構成ではありません。

Goの標準ツールチェーンのGCでは、CPUとメモリ使用量のバランスや、実際の負荷を見る必要があります。どの言語が常に最速かを決める仕組みではなく、目的の応答時間や使用量を満たすかを測ります。

メモリが増え続けるなら、オブジェクト数と参照の残る経路、キャッシュや待ち行列の容量などを調べます。GCの調整だけで、持ち続けているデータが不要と判定されるわけではありません。

ファイル・接続・ロックは、言語やライブラリが提供するclose、with、try/finally等で寿命を管理します。GCの回収時期へ業務上の終了を任せないようにしましょう。

覚え方と次の一歩

「GC」って出てきたら「たどれなくなったデータのメモリを片付ける仕組み」と思えばだいたいOK!

OSのメモリ管理と、アプリ内の回収・OSの配分の違いを比べられます。JavaScript入門で変数と参照から試すこともできます。

参考資料