ガベージコレクション
不要になったメモリを自動的に解放するランタイムの仕組み
ガベージコレクションとは
ガベージコレクション (GC) は、プログラムが使わなくなったメモリを自動的に検出・解放するランタイムの仕組みである。JavaScript (V8)、Java (JVM)、Go、Python が GC を持つ。Rust は GC の代わりに所有権システムでメモリを管理する。
GC あり vs GC なし
GC ありと GC なしの違いを以下にまとめる。
| 観点 | GC あり (JS, Java, Go) | GC なし (Rust, C) |
|---|---|---|
| メモリ管理 | 自動 | 手動 (Rust は所有権) |
| メモリリーク | 起きにくい | 起きやすい (C) |
| パフォーマンス | GC 停止 (STW) がある | 予測可能 |
| 開発速度 | 速い | 遅い (メモリを意識) |
V8 (Node.js) の GC
V8 は世代別 GC を採用している。
Young Generation (新世代 = nursery + intermediate):
新しいオブジェクトは nursery (新生領域) に確保
Scavenge を 1 回生き延びる → intermediate (まだ新世代に留まる)
さらにもう 1 回生き延びる → Old へ昇格
Old Generation (旧世代):
長寿命のオブジェクトを格納
低頻度で GC (Mark-Compact)
| 世代 | GC アルゴリズム | 頻度 | 主スレッドの停止 |
|---|---|---|---|
| Young | Scavenge (コピー GC) | 高い | 短い |
| Old | Mark-Compact (並行マーキングから開始) | 低い | 長くなりやすい |
昇格が 2 段構えになっているのは、短命なオブジェクトが大半を占めるという前提 (世代仮説) に沿わせるためである。1 回生き延びただけで旧世代へ移すと、偶然 1 回だけ生き残った短命オブジェクトが旧世代を膨らませ、コストの高い Mark-Compact の対象を増やしてしまう。
停止時間をミリ秒の固定値で語れない理由も実装側にある。Scavenge のコストは生き残ったオブジェクトの数に比例し、確保した数には比例しない。旧世代の Mark-Compact も並行マーキングから始まり、マーキング作業の大半は補助スレッドへ逃がされる。「短命なオブジェクトを大量に作ると重い」「旧世代 GC は必ず長く止まる」はどちらも現行の V8 には当てはまらず、実際に効くのは中途半端に生き残るオブジェクトを減らすことである。
メモリリーク
GC があってもメモリリークは発生する。参照が残っている限り GC は回収しない。
// ❌ メモリリーク: 寿命の長い側にリスナーを登録して要素を握り続ける
const modal = document.querySelector('#modal')!;
window.addEventListener('resize', () => layout(modal));
modal.remove(); // DOM から外しても window のリスナーが modal を参照し続けるため回収されない
// ❌ メモリリーク: クロージャが大きなオブジェクトを保持
function createHandler() {
const largeData = new Array(1000000); // 100万要素
return () => console.log(largeData.length); // largeData への参照が残る
}
// ❌ メモリリーク: グローバル変数にデータを蓄積
const cache: Record<string, any> = {};
function addToCache(key: string, value: any) {
cache[key] = value; // 際限なく増加
}
3 つに共通するのは、解除を忘れたことではなく、ルートから到達できる参照が残っていることである。mark-and-sweep はグローバルオブジェクトなどのルートから辿れないオブジェクトを回収するので、要素自身に付けたリスナーは、その要素がどこからも参照されなくなれば要素ごと回収される。逆に window や document、モジュールスコープの配列や Map のように寿命の長い側から参照が伸びていれば、DOM から外しても回収されない。漏れを追うときに探すべきは removeEventListener の呼び忘れではなく、生き残っている参照経路である。
Lambda でのメモリ管理
Lambda の MemorySize はヒープの上限を決めるだけでなく、割り当てられる vCPU も決める (メモリを増やすと CPU が比例して増える)。そのため設定値が小さいと、GC が頻発するうえに 1 回の GC 自体も遅くなり、レイテンシが二重に悪化する。逆にメモリを増やして実行時間が縮めば、課金単位である GB 秒は横ばいか、下がることもある。GC 由来のレイテンシを疑うときは、メモリを 1 段上げて実行時間を測り直すのが最初の一手になる。
メモリリークの検出
メモリリークの検出の例を示す。
# Node.js のヒープスナップショット
node --inspect app.js
# Chrome DevTools → Memory → Heap Snapshot
Rust の所有権 (GC の代替)
Rust の所有権 (GC の代替) のコード例を示す。
{
let s = String::from("hello"); // s がメモリを所有
// s を使用
} // スコープを抜けると自動的にメモリ解放 (GC 不要)
理論と実装の両面から学ぶなら関連書籍が参考になる。
この記事は役に立ちましたか?
関連用語
Rust
メモリ安全性とパフォーマンスを両立する、GC なしのシステムプログラミング言語
プロセスとスレッドの違いとは - メモリ空間 / 生成コスト / 通信方法を比較
プロセスとスレッドの違いをメモリ空間 / 生成コスト / 通信方法の 3 軸で比較。Node.js での使い分け、Lambda の実行環境、データ競合とコンテナの PID 1 の落とし穴まで解説
メモリレイアウト
プログラムのメモリ空間の構造で、スタック、ヒープ、コード領域、データ領域から構成される
メモ化
関数の計算結果をキャッシュし、同じ引数での再計算を避ける最適化手法
Iterator パターン
コレクションの内部構造を公開せずに要素へ順番にアクセスする手段を提供するデザインパターン。JavaScript の Iterator プロトコル / Generator / Async Iterator と遅延評価の落とし穴を解説
所有権と借用
Rust のメモリ管理モデルで、GC なしでメモリ安全性を保証する仕組み