ガベージコレクション

不要になったメモリを自動的に解放するランタイムの仕組み

メモリ管理ランタイム
ガベージコレクション」の技術書を見る →

ガベージコレクションとは

ガベージコレクション (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 アルゴリズム頻度主スレッドの停止
YoungScavenge (コピー GC)高い短い
OldMark-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 はグローバルオブジェクトなどのルートから辿れないオブジェクトを回収するので、要素自身に付けたリスナーは、その要素がどこからも参照されなくなれば要素ごと回収される。逆に windowdocument、モジュールスコープの配列や Map のように寿命の長い側から参照が伸びていれば、DOM から外しても回収されない。漏れを追うときに探すべきは removeEventListener の呼び忘れではなく、生き残っている参照経路である。

Lambda でのメモリ管理

LambdaMemorySize はヒープの上限を決めるだけでなく、割り当てられる 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 不要)

理論と実装の両面から学ぶなら関連書籍が参考になる。

この記事は役に立ちましたか?

関連用語

関連する記事