メモリが増え続ける
「リークしている」と決めつける前に、リーク・断片化・キャッシュ・計測ミスの4つを切り分けられるようになる。どの領域が伸びているかをRSSとヒープの差から確定させ、犯人を絞り込める。
- 増え続けている=リークとは限らない。上限のないキャッシュ、アロケータの断片化、そしてそもそもの計測対象の誤りが、同じ「右肩上がり」に見える。
- 最初の分岐はRSSとヒープ使用量の比較。両方伸びるならヒープ内のリーク、RSSだけ伸びるならネイティブ領域・断片化・スレッド/FDの積み上がりを疑う。
- 決め手はスナップショットの差分。負荷をかけてGC後の状態を2回取り、増えたオブジェクトの型と保持経路を見る。型より保持経路(誰が掴んでいるか)が答えになる。
メモリ使用量のグラフが右肩上がりで、放っておくとOOMKilledで落ちる。よくある反応は「メモリリークだ」と断定してプロファイラを起動することですが、その断定が既に1つの仮説です。同じ右肩上がりのグラフは、リーク以外に少なくとも3つの原因から生まれます。切り分けずにプロファイラを覗いても、大量のオブジェクト一覧を前に途方に暮れるだけです。
4つの候補を先に並べる
横にスクロール
| 候補 | 何が起きているか | 見分ける手がかり |
|---|---|---|
| 本物のリーク | 不要になったオブジェクトが参照され続け解放されない | GC後も減らない。時間に比例して単調増加 |
| 上限のないキャッシュ | 設計通りに保持しているが上限が無い | GC後も減らないが、キー数と相関する。実装を読めば分かる |
| アロケータの断片化 | 解放済みだがOSへ返却されず、確保済み領域が広がる | ヒープ使用量は横ばいなのにRSSだけ伸びる |
| 計測対象の誤り | そもそも見ている数字が用途違い | free の used を見ている、キャッシュを含む値を見ている等 |
「メモリが逼迫している」という報告の相当数は、free -h の used や、ページキャッシュを含む値を見ていることに起因します。Linux はファイルを読むたびに空きメモリをページキャッシュに使いますが、これは必要になれば即座に解放されるので逼迫ではありません。見るべきは available です。この読み方はfree -h の出力解剖で詳しく扱っています。まずここを潰してから先へ進んでください。
最初の分岐: RSSとヒープを並べる
決定的な分岐は、プロセス全体のメモリ(RSS)と、ランタイムが管理するヒープ使用量を並べて見ることです。この2つの動きの違いが、犯人の居場所をほぼ確定させます。
RSS ↑ かつ ヒープ使用量 ↑
→ ヒープ内で参照が残っている
=本物のリーク、または上限のないキャッシュ
RSS ↑ だが ヒープ使用量は横ばい
→ ヒープの外が伸びている
=断片化/ネイティブメモリ(バッファ・拡張ライブラリ)
/スレッド数の増加/FDやソケットの積み上がり
ヒープ使用量は各ランタイムの標準的な手段で取れます(JVM なら GC ログや jcmd GC.heap_info、Node.js なら process.memoryUsage() の heapUsed、Go なら runtime.MemStats)。GC 直後の値で比べるのが要点です。GC 前の値はゴミを含むので、増えているように見えて当然です。
メモリのグラフはのこぎり波になります。ここで見るべきは山の高さではなく、谷の高さ(GC後の下限)が回を追うごとに上がっているかです。谷が上がり続けているなら、解放されない何かが確実に積み上がっています。谷が一定なら、山が高くても健全な使い方です。この判定は、プロファイラを起動する前に無料でできます。
分岐A: ヒープが伸びている
リークかキャッシュかを決めます。両者は「参照が残っている」点で同一で、意図的かどうかだけが違います。
手順1: ヒープスナップショットの差分を取る。 負荷をかけ、GC を強制し、2点でスナップショットを取って差分を見ます。増えているオブジェクトの型が一覧されますが、ここで注意があります。
手順2: 型ではなく保持経路を見る。 増えている型が String や byte[] であることはよくありますが、それは答えではありません。知りたいのはその文字列を誰が掴んでいるかです。プロファイラの「保持経路(retaining path)」「GC ルートまでの参照」を辿ります。ここに出てくるコレクションが犯人です。典型例を挙げます。
- 上限のない
Mapをキャッシュに使っている。 追加はするが削除しない。キーがユーザーIDやリクエストIDなら無限に増えます。 - リスナー・コールバックの登録解除漏れ。 登録したまま解除しないと、そのオブジェクトと、それが参照するもの全部が生き続けます。
- スレッドローカル変数の残留。 スレッドプールではスレッドが使い回されるため、処理後にクリアしないと値が残り続けます。
- クロージャが意図せず大きなオブジェクトを掴んでいる。 小さな値だけ使うつもりが、スコープごと保持されている。
手順3: キャッシュなら上限を入れて終わり。 意図的な保持だったなら、それはバグではなく設計の欠落です。件数上限とエビクション方針を入れます(挙動はLRUキャッシュ可視化で確認できます)。
分岐B: RSSだけ伸びている
ヒープが横ばいなのに RSS が伸びる場合、疑うのは次の4つです。
1. アロケータの断片化。 大小さまざまなサイズの確保と解放を繰り返すと、解放済みの隙間が使えないまま残り、アロケータは OS へ返却できません。「使っていないのに減らない」の正体です。長時間稼働のプロセスで徐々に効いてきます。
2. ネイティブメモリ。 ランタイムのヒープ外で確保される領域——ダイレクトバッファ、圧縮/画像/暗号ライブラリの内部バッファ、JIT のコードキャッシュ、メタスペース。ヒーププロファイラには映りません。
3. スレッド数の増加。 スレッドは1本ごとにスタック領域(既定で 1MB 前後)を持ちます。スレッドが解放されずに増え続ければ、それだけで RSS は伸びます。スレッド数のグラフを重ねると一発で分かります。
4. ファイルディスクリプタ・ソケットの積み上がり。 直接のメモリ量は小さいものの、これが増え続けているなら「解放漏れが起きている」という同じ根本原因のより見つけやすい兆候です。先にEMFILEとして顕在化することもあります。
# 伸びているのがどれかを同時に並べる
ps -o pid,rss,vsz,nlwp -p <PID> # RSS / 仮想 / スレッド数
ls /proc/<PID>/fd | wc -l # FD数
増加の「形」から絞る
グラフの形状にも情報があります。
| 増え方 | 示唆 | 疑う先 |
|---|---|---|
| 時間に正確に比例 | 定期処理のたびに積む | cronやスケジューラ、定期ポーリングの登録漏れ |
| リクエスト数に比例 | 1リクエストにつき一定量残る | リクエストスコープの解放漏れ、キャッシュキーの肥大 |
| 階段状に跳ねる | 特定の操作でまとめて確保 | バッチ、大きなファイル読み込み、一括クエリ |
| 再起動直後だけ急増し横ばい | ウォームアップ | 健全。リークではない |
最後の行が重要です。起動直後に急に増えてから平らになるのは正常です。JIT のウォームアップ、接続プールの確立、キャッシュの初期充填で、そこを切り取ると「増え続けている」ように見えます。判定するには十分長い観測窓が要ります。
まとめ
- 右肩上がり=リークではない。リーク・上限のないキャッシュ・断片化・計測対象の誤りの4候補をまず並べる。最頻は計測対象の誤りで、
freeはusedではなくavailableを見る。 - 最初の分岐は RSS とヒープ使用量を並べること。両方伸びればヒープ内、RSS だけならヒープ外。
- グラフは山ではなく谷(GC後の下限)が上がっているかを見る。プロファイラを起動する前に無料でできる判定。
- ヒープ内なら型ではなく保持経路を辿る。犯人は上限のない Map・解除漏れのリスナー・スレッドローカル・クロージャのいずれかであることが多い。
- RSS だけなら断片化・ネイティブメモリ・スレッド数・FD を並べて確認する。スレッド数のグラフは特に見落とされやすい。
- 起動直後の急増と横ばいはウォームアップであって異常ではない。判定には十分長い観測窓が要る。
トラブルシュート実戦の記事ガイド
メモリが増え続けるを実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
トラブルシュート
比較で見る軸
難易度: advanced / カテゴリ: トラブルシュート実戦 / タグ数: 5
導入後に効く点
最初の分岐はRSSとヒープ使用量の比較。両方伸びるならヒープ内のリーク、RSSだけ伸びるならネイティブ領域・断片化・スレッド/FDの積み上がりを疑う。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- トラブルシュート実戦
- タグ数
- 5
判断チェックリスト
- 自社の用途が「トラブルシュート / メモリ」に近いか確認する。
- 強みである「増え続けている=リークとは限らない。上限のないキャッシュ、アロケータの断片化、そしてそもそもの計測対象の誤りが、同じ「右肩上がり」に見える。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。