FIG-082

メモリリーク — GCがあるのに、なぜ溢れるのか

OS 2026.09.01 公開 読了 約11分

メモリリークは「使い終わったのに、片付けられないまま残り続けるメモリ」のことです。バケツの水を汲むたびに捨て忘れると、やがて溢れる。プログラムでも同じことが起き、最後はメモリ不足で落ちます。

ここで多くの人がつまずくのが、「GC(ガベージコレクタ)があるのに、なぜリークするのか」という点です。下の図1で、GCを動かしたままリークを起こしてみてください。答えが見えます。

MEMORY LEAK — 捨て忘れた水で、バケツが溢れる
HEAP(プログラムが使えるメモリ)
ROOT
(グローバル変数)
const cache = [] 0 件を掴んだまま
参照が残っている(消せない) 参照が切れた(ゴミ) 空き
HEAP USAGE — 時間とともにどう動くか
上の赤い線に触れたところで OOM(メモリ不足)
使用中 0 / 64
GCが回収した数 0
回収できなかった数 0
READY
GCは動いている。それでもリークする
「機能を1回実行する」を押すと、処理のためにメモリを4マス使います。いまは使い終わったオブジェクトをグローバル変数 cache に入れっぱなしにするバグ入りの状態です。GCは自動で回り続けています。何度か押して、空きが戻るかどうか見てください。
図1 — GCが消せるのは「どこからも辿れなくなったもの」だけ

GCが消すのは「ゴミ」ではなく「辿れないもの」

ガベージコレクタは、開発者の気持ちを読んでくれません。GCの判断基準はただ一つ、ルート(グローバル変数や実行中の関数の変数)から参照を辿って到達できるかです。

辿り着けたものは「まだ使う気だ」とみなされ、絶対に消されません。辿り着けなかったものだけが回収されます。つまり「もう使わないオブジェクト」と「GCが消せるオブジェクト」は別物だということです。

図1でリークが止まらないのはこのためです。あなたはもう使うつもりがなくても、cache という配列が掴んだままなので、GCから見れば現役のデータ。だからGCを何度走らせても1マスも空きません。上のタブを「参照を捨てる」に切り替えると、同じ処理なのに使用量が元に戻ります。

だから、リークは「掴みっぱなし」を探す作業になる

現実のメモリリークは、ほとんどが意図せず参照を持ち続けている場所です。よくあるのは次のような形です。

1つ目は、際限なく伸びるグローバルなコレクション。キャッシュのつもりで配列やMapに入れ続け、捨てる条件を書き忘れるパターンです。上限やTTLが無いキャッシュはリークと同じ意味になります。

2つ目は、外し忘れたイベントリスナーやタイマー。要素を画面から消しても、リスナーが登録されたままだと、その要素も一緒に掴まれ続けます。setInterval を止め忘れるのも同じことです。

3つ目は、閉じ込められたクロージャ。小さなコールバックが、たまたま大きなオブジェクトを参照していると、その大きな方まで丸ごと生き残ります。

用語ミニ辞書
ヒープ
プログラムが動的に確保するメモリ領域。図1のマス目にあたる。
ルート
参照を辿る出発点。グローバル変数や実行中の関数のローカル変数など。
到達可能性
ルートから参照を辿って届くかどうか。GCが生死を決める唯一の基準。
GC
到達できないメモリを自動で回収する仕組み。参照が残っていると何もできない。
OOM
Out Of Memory。空きが尽きて確保に失敗し、プログラムが落ちること。

まとめ

メモリリークは「もう使わないのに、参照だけが残っている」状態です。GCがあっても防げないのは、GCが見ているのが使う気があるかではなく辿れるかだから。だから対処も一点に絞られます――掴んだままの参照を、どこで手放すかを決めること。配列から消す、リスナーを外す、タイマーを止める。それだけで、GCは初めて仕事ができるようになります。