ガベージコレクションの仕組み — 参照が残ると、なぜ片付かない?
カートを空にしても、控えからたどれる
まずは、カートを空にしても残る「控え」を見る
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入門で変数と参照から試すこともできます。
参考資料
- MDN:Memory management — JavaScriptの参照と到達可能性、回収方式
- Python:Data model — CPythonの参照カウント、回収時期・ファイルを明示的に閉じる条件
- Go:GC Guide — 標準ツールチェーンの並行GCとCPU・メモリ・遅延の負担
- Rust Book:What is Ownership? — 所有権とスコープ終了時のdrop、実行時の解放