Memory Leak とは?二度と戻らないメモリ
memory leak は、プログラムがもう必要としないメモリを握り続け、時間とともにメモリ使用量が増え、やがて枯渇するときに起こります。
memory leak は最初は劇的な crash ではありません。ゆっくりとした上昇です。プログラムはメモリを確保し続け、その一部を決して手放さないため、使用量がじわじわと上がります。最終的にプロセスは利用可能なメモリを使い果たし、crash するか kill されます。leak は手動メモリ管理の言語(解放を忘れる)でも、garbage collection のある言語(参照を生かし続ける)でも起こります。
leak とは本当は何か
leak はまだ確保されているが、二度と使われることも解放されることもないメモリです。手動の言語では対応する free のない確保であり、garbage collection の言語では何かがまだ参照しているために collector が回収できないオブジェクトです。
一般的な原因
- 決して切り詰められない、増え続ける cache やリスト。
- 追加されるが決して削除されない event listener や subscription。
- 大きなオブジェクトを捕捉して保持し続ける closure。
- プログラムの生存期間を通じてエントリを蓄積するグローバルな registry。
なぜ leak は厄介なのか
leak が何かをすぐ壊すことはめったにありません。寿命の短いプログラムは leak が問題になる前に終わるかもしれません。しかし長時間動くサーバーはゆっくりと劣化し、やがて倒れます。多くの場合、問題のコードが動いてから何時間も後にです。
leak を見つける
heap snapshot と memory profiler は、どのオブジェクトが増え続けているか、そして何がそれらへの参照を保持しているかを示します。長時間の実行にわたってメモリを追跡し、着実な上昇傾向を探すのが、leak の存在を確認する通常の方法です。
簡単な例
リクエストごとにモジュールレベルの配列へ項目を push し、決して削除しないと、メモリが leak します。配列は永遠に増え続け、それらの項目が二度と読まれなくても、配列が保持するものは何も回収できません。
CI における memory leak
leak のある test suite は runner のメモリ上限を越えて OOM-kill されることがあり、多くの場合は大規模な実行でのみ起こるため、flaky な失敗に見えます。より大きな runner(Latchkey)は症状を先延ばしにし、一時的な OOM kill を自動 retry しますが、永続的な leak はやはり修正が必要です。メモリを増やしても crash を先送りするだけだからです。
重要なポイント
- memory leak は決して解放されない確保済みメモリで、時間とともに使用量が増えます。
- leak は手動と garbage collection の両方の言語で、多くの場合残り続ける参照によって起こります。
- CI では OOM kill として表面化し、しばしば flaky な失敗と誤解されます。