~現場で本当に使える、Javaメモリ解析の完全ガイド~
はじめに:なぜメモリリーク調査が重要なのか?
Javaのプログラムはガーベジコレクション(GC)によって、使われなくなったオブジェクトを自動的に回収します。
このおかげで、他の言語のように「手動でメモリを解放する」作業は不要です。
しかし、それでもメモリリークは起こります。
「もう使わないはずのオブジェクトが、どこかで参照され続けている」
そのせいでGCが回収できず、ヒープ(Heap)が徐々に圧迫されていく──
これがJavaにおけるメモリリークの正体です。
メモリリークは一見、些細な問題に見えます。
ですが、長時間稼働するサーバーやWebアプリでは、
数時間〜数日でアプリが落ちる といった深刻な障害に発展することもあります。
そんなときに頼りになるのが、ヒープダンプ(Heap Dump) です。
ヒープダンプとは?
ヒープダンプとは、JVMのヒープメモリ内に存在するすべてのオブジェクトをスナップショットとして保存したものです。
簡単に言えば、「今この瞬間、JVMの中に何がどれだけ存在しているか」を丸ごと写し取ったメモリの写真です。
ヒープダンプを取得・解析することで、
「どんなクラスのインスタンスがどれくらいあるのか」
「どこから参照されていて、なぜGCが回収できないのか」
を可視化することができます。
ヒープダンプを使った調査の流れ
ヒープダンプ調査は、次の4ステップで行います。
- ヒープダンプを取得する
- ツール(MATなど)で開く
- 参照関係とメモリ保持量を分析する
- コードを修正して再検証する
それでは、順を追って詳しく見ていきましょう。
ステップ①:ヒープダンプを取得する
ヒープダンプを取得する方法は主に3つあります。
1. OutOfMemoryError発生時に自動取得
もっとも簡単な方法は、JVMの起動オプションで自動出力を有効にしておくことです。
|
1 2 |
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump.hprof |
これを設定しておけば、アプリが OutOfMemoryError を出したとき、
その瞬間のヒープ内容を .hprof ファイルとして出力してくれます。
2. 実行中のプロセスから手動で取得
本番環境や開発中に手動でダンプを取りたい場合は、jmap または jcmd コマンドを使います。
|
1 2 |
jmap -dump:live,format=b,file=heap_dump.hprof <pid> |
もしくは:
|
1 2 |
jcmd <pid> GC.heap_dump filename=heap_dump.hprof |
<pid> は対象のJavaプロセスIDです。
これらのコマンドは稼働中のアプリに一時的な負荷をかけるため、
実行タイミングには注意が必要です。
3. ツールから取得(VisualVM など)
GUIツール(例:VisualVM)を使えば、ワンクリックでヒープダンプを取得できます。
初心者にはこちらが最も安全で簡単です。
ステップ②:ヒープダンプを解析する(Eclipse Memory Analyzer)
ヒープダンプを取得したら、次は「Eclipse Memory Analyzer(MAT)」を使って解析します。
これはJavaのメモリ問題を可視化する定番ツールで、無料で使えます。
1. ヒープダンプを開く
MATを起動して「File → Open Heap Dump」から .hprof ファイルを選択します。
数秒〜数分後、メモリの全容が解析され、
クラス別のインスタンス数やサイズ、参照関係が一覧で表示されます。
2. 「Leak Suspects Report」を確認する
まずはMATが自動生成してくれる「Leak Suspects Report(リーク疑いレポート)」を開きましょう。
これは、MATが「このオブジェクトがリークしている可能性が高い」と判断した候補をレポート形式で提示してくれる機能です。
- どのクラスがどれだけメモリを使っているか
- 誰がそのクラスを保持しているのか
- どのパスでGCが到達できないか
といった情報が一目でわかります。
3. 「Histogram」で全体傾向を把握
「Histogram」ビューでは、
クラスごとのインスタンス数と合計サイズを確認できます。
たとえば、java.util.HashMap のインスタンスが異常に多い場合、
何かのキャッシュやコレクションが削除されずに残っている可能性があります。
4. 「Dominator Tree」で保持関係を確認
「Dominator Tree」は、どのオブジェクトが他のオブジェクトを保持しているかを木構造で表示します。
メモリを大量に消費しているオブジェクトを特定するには、このツリーが非常に役立ちます。
- Shallow Heap:オブジェクト自体のサイズ
- Retained Heap:そのオブジェクトが間接的に保持している全オブジェクトの合計サイズ
Retained Heap が大きいクラスは、リークを疑う価値ありです。
ステップ③:メモリリークの原因を突き止める
MATでの解析結果から、「なぜGCが回収できないのか」を追っていきます。
以下は、実際によくあるリークパターンです。
パターン①:静的フィールドが参照を保持している
|
1 2 3 4 5 6 7 8 |
public class CacheManager { private static final List<Data> cache = new ArrayList<>(); public static void add(Data data) { cache.add(data); } } |
cache にデータを追加し続けると、GCが回収できません。
MATで見ると、CacheManager クラスが巨大な ArrayList を保持しているのが分かります。
対策
- 不要になったデータは削除する (
cache.clear()など) - キャッシュには
WeakHashMapを使う
パターン②:リスナー・イベント登録の解除忘れ
GUIアプリやWebSocketなどでよくあるのが、リスナー登録を解除しないまま残すケースです。
対策
- イベント解除メソッド(
removeListener()など)を必ず呼ぶ - WeakReferenceを使ってリスナーを管理する
パターン③:コレクションに古いデータが溜まり続ける
たとえば次のようなキャッシュクラス。
|
1 2 3 4 5 6 |
private Map<String, Object> cache = new HashMap<>(); public Object get(String key) { return cache.get(key); } |
このようなコードでは、キャッシュの中身が増え続けます。
MATでは、HashMap が異常に大きくなっているのがヒントになります。
対策
- 使用期限付きキャッシュを導入する
WeakHashMapやLinkedHashMapの削除機構を活用
パターン④:ThreadLocalのremove忘れ
ThreadLocalはスレッド単位で値を保持しますが、
スレッドプールを使っている環境では、スレッドが再利用され続けるため、
ThreadLocalの中身がずっと残ってしまうことがあります。
|
1 2 3 |
private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial( () -> new SimpleDateFormat("yyyy-MM-dd")); |
対策
- 使用後に必ず
remove()を呼ぶ - もしくは毎回インスタンスを生成する
パターン⑤:クラスローダーリーク
Webアプリを再デプロイしても、古いクラスローダーが残り続けるパターンです。
静的フィールドやシングルトンが、旧クラスローダーを参照していると発生します。
対策
- 静的変数にアプリ固有クラスを保持しない
- シングルトンを使う場合は解放処理を入れる
ステップ④:修正・再検証・再ダンプ
原因を特定したら、修正を行い、再びヒープダンプを取得して比較します。
再ダンプ時に確認するポイント:
- 該当クラスのインスタンス数が減っているか
- Retained Heapサイズが減少しているか
- 長時間動かしてもヒープ使用量が安定しているか
修正が正しい場合、これらの指標は明確に改善します。
逆に変化がない場合は、まだどこかで参照が残っている可能性があります。
実践Tips:現場で使える調査のコツ
- GCログを有効化して傾向を掴む
-Xlog:gc*オプションを使うと、GC頻度やメモリ変化を時系列で確認できます。 - 時間を変えて複数回ヒープダンプを取得
「開始直後」「1時間後」「3時間後」など複数のスナップショットを比較すると、
どのクラスが増えているかが明確にわかります。 - VisualVMやMATを併用する
VisualVMでリアルタイム監視 → 異常を検知したらMATで詳細分析、が理想の流れです。 - クラス名だけで判断しない
同じクラスでも保持しているデータが違うことがあります。
「どのインスタンスが、どこから参照されているか」を見ることが重要です。
メモリ解析力が「現場での強み」になる理由
メモリリークを見抜けるエンジニアは、現場でも一目置かれます。
なぜなら、多くの人が「GC任せ」で済ませてしまうからです。
でも、
- ヒープダンプを取得し、
- 参照をたどって、
- 原因を論理的に特定できる
という力は、「問題解決力」そのものです。
このスキルがあるだけで、
パフォーマンス改善・運用監視・トラブルシューティングなど、
どんなチームでも重宝されます。
Javaプログラマーを目指すあなたへ
ヒープダンプ解析は難しそうに見えますが、やってみると意外にシンプルです。
コツは、「焦らず、ひとつずつ追う」こと。
そして、こうしたメモリ管理の基礎をしっかり理解したいなら、
まずは 絶対にJavaプログラマーになりたい人へ。 を読むことをおすすめします。
さらに、実際の現場でのコードレビューや転職サポートまで受けたい方は、
サイゼントアカデミー が最適です。
自己学習で基礎を固め、
サイゼントアカデミーで実践力を磨く。
この2ステップで、あなたは確実に“メモリを理解して扱えるJavaエンジニア”になれます。
まとめ
- ヒープダンプは「JVMのメモリの写真」
- MATを使えば、どのクラスがメモリを保持しているか一目でわかる
- 静的変数、キャッシュ、リスナー、ThreadLocalがリークの主犯になりやすい
- 修正→再ダンプ→比較で効果を確認する
- GC任せにせず、メモリを「設計」する意識が大切
終わりに
メモリ解析は、「問題が起きてから」取り組む人が多いですが、
本当は「設計の段階でリークを防ぐ」ことが理想です。
ヒープダンプ調査の技術を身につければ、
障害対応だけでなく、再発防止・設計改善にもつなげられます。
次回は、「GCログとヒープダンプを組み合わせたメモリ最適化の実践編」をお届けします。
あなたのJavaアプリが、より速く・安定して動くようになるはずです。


コメント