✅ GCログとヒープダンプを組み合わせたメモリ最適化の実践編

Java

~Javaプログラムを“止まらず・軽く”動かすためのメモリ戦略~

はじめに:なぜ「GCログ+ヒープダンプ」が強いのか?

Javaのプログラム運用において、
「メモリが徐々に増えていく」「応答が遅くなる」「アプリが突然止まる」といった問題に遭遇することは珍しくありません。
これらの原因は多くの場合、ガベージコレクション(GC)やメモリリークにあります。

しかし、それらをただ漠然と「メモリ使いすぎだな」と片付けてしまうと、
根本的な原因が見えずに再発を繰り返します。
そこで役立つのが、**「GCログ」+「ヒープダンプ」**という2つの観察手段を組み合わせた調査です。

  • GCログ:どのタイミングでGCが起きたか、どれだけ停止時間があったか、ヒープの使用量はどう変わったか。
  • ヒープダンプ:その時点でヒープ中にどんなオブジェクトがどれだけいて、なぜ回収されないか。

これらを組み合わせることで、
「なぜGCが頻繁に起きるか」「なぜヒープが減らないか」「どのオブジェクトが原因か」
まで深掘りできます。そして、単なる対症療法ではなく、設計・運用レベルでの最適化に繋がります。

今回はその「実践編」として、具体的な手順・ポイント・コード例も含めて解説します。


全体流れ:4つのステップで進める

以下のような流れで進めると、調査から改善まで無駄なく実施できます。

  1. GCログを取得・観察する
  2. 異常を発見したらヒープダンプを取得・解析する
  3. 原因を特定し、コードまたは設定を改善する
  4. 再度ログ・ダンプを取得して、改善効果を検証する

このサイクルを回すことで、確実にメモリ最適化が進みます。


ステップ①:GCログを取得・観察する

GCログの取得方法

JavaバージョンやGC方式によってログ取得のオプションは異なりますが、基本的な例として:

  • Java 8以前の場合(例:G1GC) -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/path/to/gc.log
  • Java 9以降の場合 -Xlog:gc*=info:file=/path/to/gc.log:time,uptime,level,tags

ログのローテーションやサイズ制限も忘れずに。

ログで注目すべきポイント

GCログを見て「異常だな」と感じる兆候には以下があります。

  • GCが頻繁すぎる:オブジェクトを大量に生成・破棄して若世代が頻繁に回収されている。
  • GC停止時間(STW:Stop The World)が長い:アプリが数百ミリ秒~数秒以上停止している。
  • 古い世代(Old Generation)が増え続けて減らない:ヒープ使用量は戻らず、フルGCが発生している。
  • アロケーション失敗(Allocation Failure)で頻繁にGCが起きている:若世代に割り当てる空きが無い。

例:ログから何が分かるか

この例から分かること:

  • 若世代GCは8msと短くて許容範囲
  • しかし直後にフルGCが320ms発生。ヒープがほぼ満杯(95M/100M)でアロケーション失敗 → 即・全体回収の流れ
  • =若世代で回収できず、多くのオブジェクトが旧世代へ昇格 → フルGCの引き金に

このようなパターンを見つけたら、次のステップへ進む合図です。


ステップ②:ヒープダンプを取得・解析する

GCログで「怪しい」と感じたら、ヒープダンプを取得して具体的に原因を突き止めましょう。

ヒープダンプの取得方法

  • JVMオプションで自動取得: -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump.hprof
  • 実行中プロセスから: jcmd <pid> GC.heap_dump filename=heap_dump.hprof

ツールでの解析(例:Eclipse Memory Analyzer (MAT))

ダンプ取得後は以下のように解析します。

  1. ダンプをMATで開く
  2. 「Leak Suspects Report」で怪しいクラスをピックアップ
  3. “Histogram”でインスタンス数・サイズを確認
  4. “Dominator Tree”でどのオブジェクトが大きな保持量を持っているか分析

ログとの組み合わせで見えること

  • GCログで「旧世代増加/フルGC頻発」を確認
  • ヒープダンプで「どのクラスのインスタンスが増えているか」「どこから参照されているか」確認
  • これにより、「なぜ旧世代が増えたか」「どのオブジェクトが放置されていたか」が分かる

ステップ③:原因特定と改善策

原因に応じて、コード修正・設定変更を行います。以下は代表的な改善パターンです。

ケースA:短命オブジェクトが多すぎて若世代が飽和

  • 若世代が頻回にGCされ、旧世代へ昇格してしまう
  • → ログ:若GC頻発+旧世代使用率増加
  • → 対策:
    • オブジェクト生成量を減らす(再利用可能なオブジェクトを検討)
    • 若世代サイズを調整 (-XX:NewSize, -XX:MaxNewSize)
    • GC方式を見直す(例:G1GCなら -XX:InitiatingHeapOccupancyPercent を調整)

ケースB:メモリリークで旧世代が戻らない

  • ヒープダンプで「大量のインスタンスが参照され続けている」クラスを確認
  • → ログ:フルGCしてもヒープが減らない/使用率上昇のまま
  • → 対策:
    • 不要な参照を切る(static、コレクション、リスナーなど)
    • キャッシュ設計を見直す(期限付き、弱参照)
    • ThreadLocal やクラスローダーのリークをチェック

ケースC:ポーズ時間(STW)が長すぎて応答に影響

  • ログ:STWが数百ミリ秒以上/応答遅延が出ている
  • → 対策:
    • 小さめのヒープ/リージョン設計にする
    • 低遅延GC方式(例:ZGC)を検討
    • ヒープ断片化の対策、オブジェクト寿命別分離

ステップ④:再検証と運用モニタリング

修正後、必ず再評価を行いましょう。

  • 修正前後でGCログを比較:GC頻度・ポーズ時間・旧世代使用率が改善されているか
  • 再度ヒープダンプを取得:問題クラスのインスタンス数・保持量が減っているか
  • 運用中もモニタリングを継続:
    • ヒープ使用量の推移(安定しているか)
    • GC停止時間の傾向
    • 応答時間・スループットとの相関

このように「改善 → 検証 →運用監視」を回すことで、メモリ最適化が定着します。


初心者プログラマー向けポイント

  • まずは“ログを読む”ことに慣れよう。
    GCログを眺めれば「何か増えてるな/止まってるな」が徐々にわかるようになります。
  • ツール(MAT)を使うと“オブジェクトが残っている理由”が可視化できます。
  • 最初から完璧にする必要はありません。小さな改善(キャッシュのクリア、static参照の整理)から実施しましょう。
  • 学習は理論だけでなく「実際に自分でログを取る」「ダンプを開く」ことが大事です。
  • まずは書籍で基礎を学び、実践に移ると早く身につきます。
    絶対にJavaプログラマーになりたい人へ。
    → コードレビューや転職支援も考えるなら サイゼントアカデミー へ。

まとめ

  • GCログとヒープダンプを組み合わせることで、原因の“見えない壁”を可視化できる。
  • 調査→分析→改善→再検証というサイクルを回すことが、メモリ最適化の実践パターン。
  • 「GCが頻繁」「ヒープが戻らない」「ポーズが長い」と感じたら、まずログを見て、そのあとダンプを使って深掘り。
  • 小さな改善が、長時間稼働するシステムでは大きな差になる。

次回は、「クラウド環境(コンテナ・Kubernetes)でのGCログ&ヒープダンプ活用の実践」についてお届けします。
あなたのJavaスキルがさらに深まることを願っています。

コメント

タイトルとURLをコピーしました