Troubleshoot Memory Issues in Your Java Apps

原文はこちら。
The original article was written by Chi Wang (Software Engineering Lead, EInstein.AI – Salesforce.com) and edited by Dianne Siebold (Principal Technical Writer at Salesforce.com).
https://engineering.salesforce.com/troubleshoot-memory-issues-in-your-java-apps-719b1d0f9b78

How to find out the root cause for a memory leak

Javaにおけるメモリ周りのトラブルシューティングは、よく議論されているトピックです。既存のブログ記事には多くの情報がありますので、なぜ別の記事を書くのかと疑問に思うかもしれません。実際、多くのブログ記事では、さまざまなツールやテクニックが紹介されています。しかし、驚くべきことに、そこにある情報の多くは深みに欠け、体系的な見解を提供しておらず、方法論を提供していません。そのため、開発者がこれらの知識を使って実際の問題を解決することは難しいのです。

これは、Javaにおける現実のメモリ問題の発見と修正についての2回シリーズの第1回目の投稿です。この記事では、メモリ問題をトラブルシューティングするためのツールや方法論を紹介します。次回の記事 ”Fix Memory Issues in Your Java Apps” では、見つけたメモリ問題を修正する方法と、本番環境でのトラブルシューティングを処理する方法について説明します。

Fix Memory Issues in Your Java Apps
https://engineering.salesforce.com/fix-memory-issues-in-your-java-apps-c261046bae52
https://logico-jp.io/2021/01/05/fix-memory-issues-in-your-java-apps/

このブログ記事では、以下の方法を学びます。

  1. Javaのメモリリークの理解
  2. メモリリークのトラブルシューティング

Understand Java Memory Leaks

メモリリークとは何でしょう?メモリリークは、アプリケーションで使用されていないにもかかわらず、ガベージコレクタでメモリから削除できないオブジェクトがある場合に発生します。ガベージコレクタ(GC)が参照されたオブジェクトを削除できないので、より多くのメモリリソースが消費され続けます。

Java の大きな利点の 1 つは、組み込みのガベージコレクションです。Javaプログラムが実行されると、ヒープにオブジェクトが作成されます。ガベージコレクタは、ヒープ内のすべてのメモリ割り当てを追跡し、使用されていないオブジェクトを自動的に削除します。ガベージコレクションは非常に優れた機能を持っていますが、絶対確実というわけではありません。ガベージコレクションがあるにもかかわらず、メモリリークすることがあります。

C++とは異なり、Javaのメモリリークの問題は、メモリ割り当ての追跡を失うことではなく、不要になったオブジェクトが不必要にヒープ内で参照され、ガベージコレクタがそれらを削除できなくなるために発生します。メモリリークを発見するためのツールやテクニックを見てみましょう。

How to detect a memory leak

メモリリークを検出するには、2つの方法があります。1つ目は、アプリがクラッシュしてログやコンソール出力にOutOfMemoryError例外が表示されるまで待つ、というものです。2つ目は、私がいつもやっていることですが、JVMのold genスペースサイズの傾向をチェックすることです。

JVMヒープでは、old genスペースは長寿命のオブジェクトのために予約されています。old genスペースは、メジャーGCの後にクリーンアップされるべきです。そのため、フルGC(GCログをチェックしてください)後にold genスペースが増え続けているのであれば、ほとんどの場合、アプリケーションにメモリリークがある可能性が高いです。異なるヒープ空間と異なるタイプのGCの詳細については、”understanding the Java memory model”(Javaメモリモデルを理解する)の記事を参照してください。

Understanding Java Memory Model
https://medium.com/platform-engineer/understanding-java-memory-model-1d0863f6d973

JVM設定を使ってGCログを有効にし、プロファイラをアタッチしてJVMメモリ使用量を取得するか、アプリケーションから直接JVMメトリクスを測定できます。

Typical Causes for Memory Leaks

メモリリークの原因はたくさん考えられます。ここでは、最も典型的なものを紹介します。詳細な説明は、Understanding Memory Leaks in Javaの記事をチェックしてください。

Understanding Memory Leaks in Java
https://www.baeldung.com/java-memory-leaks

  • 長寿命オブジェクト(通常はstaticフィールド)で大きなオブジェクトを参照している
  • 接続やストリームなどのリソースクローズを忘れている
  • キャッシュに格納されたオブジェクトの equal() または hashCode() の実装が不適切である
  • ThreadLocal 変数の削除を忘れている
  • ClassLoaderMemory Leak(例: JDK-6543126)
  • オフヒープ(ネイティブメモリ)のメモリリーク

JDK-6543126 : Level.known can leak memory
https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6543126

Troubleshoot Memory Issues

このセクションでは、アプリケーションの現在のJVM設定、メモリダンプファイル(スナップショット)、ネイティブスペースでのJVMのメモリ割り当てなど、トラブルシューティングに不可欠なデータを収集するための便利なツールをいくつか紹介します。また、ほぼすべてのメモリ問題に適用できる一般的なトラブルシューティング戦略も紹介します。

高負荷の本番環境でのメモリリークをトラブルシューティングするために、その他にもいくつか提案できるものがあります。次回の記事(”Fix Memory Issues in Your Java Apps”の”Troubleshoot in Production”セクション)をチェックしてください。

Fix Memory Issues in Your Java Apps
https://engineering.salesforce.com/fix-memory-issues-in-your-java-apps-c261046bae52
https://logico-jp.io/2021/01/05/fix-memory-issues-in-your-java-apps/

Assemble your troubleshooting toolbox

JVMのメモリに関連する問題をトラブルシューティングするための有用なツールをご紹介します。

Get your Java application’s JVM settings

調査を開始する前に、まずアプリケーションの現在のJVM設定(最大、最小ヒープサイズ、metaspaceのサイズ、GCリサイクル時間やリサイクルratioなど)を確認したいものです。これらの設定はアプリケーションパフォーマンスを調べる上で重要です。アプリケーションのJVM設定はコマンドラインツールjpsを使って取得できます。このツールは対象システムの全ての測定可能なJVMをリスト化します。

jps
https://docs.oracle.com/javase/jp/15/docs/specs/man/jps.html
https://docs.oracle.com/en/java/javase/15/docs/specs/man/jps.html

$ jps -v
44040 App -Xms2G -Xmx2G -XX:MetaspaceSize=1G -XX:MaxMetaspaceSize=1G -XX:NativeMemoryTracking=detail -javaagent:/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar=57671:/Applications/IntelliJ IDEA.app/Contents/bin -Dfile.encoding=UTF-8

Take heap snapshot

jmapという別のツールを使って、1個のJVMプロセスのヒープのメモリレイアウトの詳細(メモリダンプもしくはスナップショットとも呼ばれます)を出力しましょう。これもまたコマンドラインツールなので、リモートサーバーで実行できます。

jmap
https://docs.oracle.com/javase/jp/15/docs/specs/man/jmap.html
https://docs.oracle.com/en/java/javase/15/docs/specs/man/jmap.html

$ jmap -dump:file=example.hprof 49952      // プロセスID (この例では49952)はjpsで取得したもの)
Dumping heap to /Users/chi.wang/workspace/example.hprof ...
Heap dump file created

jmapを使って1個のJVMプロセスのオブジェクトヒストグラムを出力することもできます。このヒストグラムから、どれほどのメモリを異なる種類のオブジェクトが利用しているかがわかります。スナップショットと比べて、軽量かつ高速に収集できますが詳細な情報を収集できるわけではありません。とはいえ、JVMプロセスの環境がメモリ上で短期間の場合は有用です。

Examine object allocations in the heap

メモリプロファイラは、ヒープの中を見ることができるように設計されています。”A Guide to Java Profilers” (Baeldung) では以下のように説明しています。

“A Java Profiler is a tool that monitors Java bytecode constructs and operations at the JVM level. These code constructs and operations include object creation, iterative executions (including recursive calls), method executions, thread executions, and garbage collections.”
Java Profilerは、Javaバイトコードの構成と操作をJVMレベルで監視するツールです。これらのコード構成と操作には、オブジェクトの作成、反復実行(再帰的呼び出しを含む)、メソッドの実行、スレッドの実行、およびガベージコレクションが含まれます。

A Guide to Java Profilers – https://www.baeldung.com/java-profilers

実際には、プロファイラは、Javaのメモリ関連の問題をトラブルシューティングする際に最も頻繁に使用されるツールです。プロファイラを使用して JVM ヒープダンプファイルを調べたり、プロファイラをプロセスにアタッチしてライブ・デバッグを行うことができます。プロファイラは、JVMの詳細(GC、メモリ割り当て、スレッド、CPU)を追跡し、その情報を素敵なUIに整理してくれるため、トラブルシューティングがしやすくなります。この記事ではYourkitプロファイラを使用していますが、ここで紹介したスキルはどのプロファイラにも適用できます。

YourKit Java Profiler Features
https://www.yourkit.com/java/profiler/

Check JVM native memory usage

ヒープ以外にも、JVMはスレッドスタック、コード、シンボルなどのためにネイティブメモリを占有します。ヒープが問題なさそうに見えたら、JVMによるネイティブメモリの使用状況(metaspace、スレッドスタックのサイズなど)をチェックしてみましょう。

ネイティブ・メモリ・トラッキング(NMT)は、JVMがネイティブ・メモリ空間でどのように動作するかを調べるためのツールです。NMTの使い方は簡単です。JVMの設定で-XX:NativeMemoryTrackingというフラグを追加し、jcmdで結果を確認するだけです。

Native Memory Tracking (NMT)
https://docs.oracle.com/javase/jp/15/vm/native-memory-tracking.html
https://docs.oracle.com/en/java/javase/15/vm/native-memory-tracking.html

// enable NMT
-XX:NativeMemoryTracking=summary or -XX:NativeMemoryTracking=detail.// set baseline and check diff
jcmd <pid> VM.native_memory baseline
jcmd <pid> VM.native_memory detail.diff / summary.diffjcmd <pid> VM.native_memory summary

NMTはサードパーティのネイティブコードやJDKクラスライブラリを追跡しないことに注意してください。その名前から、アプリケーションのすべてのネイティブメモリ割り当てを追跡していると思われるかもしれませんが、そうではないため、紛らわしいかもしれません。例えば、アプリケーションがTensorflow JNIライブラリを使用している場合、グラフやセッション用のモデルをロードする際に多くのネイティブメモリを消費しますが、これらのオブジェクトはすべてNMTによって追跡されません。次回の投稿 “Fix Memory Issues in Your Java Apps” の “Detect and Solve a Native Memory Leak” で、この手の問題を解決する方法を説明しています。

Use the recommended JVM settings for troubleshooting

ここでは、トラブルシューティングに役立ついくつかの提案されたJVM設定を紹介します。jpsツールを使用して、アプリケーションのJVM設定を確認できます(上記の “Get your Java application’s JVM settings” のセクションを参照してください)。各GCアクティビティの後のアプリケーションのメモリステータスがメモリリークがあるかどうかを示す可能性があるため、GCログを有効にすることは非常に重要です。

// jvm space size settings
-Xms -Xmx     // min and max heap size 
-XX:MetaspaceSize -XX:MaxMetaspaceSize    // min and max metaspace 
-XX:NativeMemoryTracking=detail      // enable native memory tracking
-XX:MaxDirectMemorySize      // limit direct memory allocation // enable gc log
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC 
-Xloggc:logs/gc.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=1 
-XX:GCLogFileSize=1M // enable OOM dump 
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath={any_path}/heap_dump.hprof

General Steps to Troubleshoot a Memory Leak

ツールやJVMの設定を熟知したあとは、以下の手順でアプリケーションのトラブルシューティングをしましょう。

1. Get familiar with application code and user scenarios.(アプリケーションコードとユーザーシナリオの熟知)

プロファイラが発見した症状をコード内の根本原因と相関させるには、アプリケーションのコードに精通している必要があります。通常、プロファイラを使用する前にコードを読み込んで主要なワークフローをトレースします。こうすることで、ラージ・オブジェクトやデータ・バッチ・オブジェクトがどこで作成され、メモリ内で参照されているかを認知できます。

2. Prepare measurements for JVM’s young/old gen space consumption.(JVMのyoung/old genスペースの消費量測定の準備)

JVMメトリックをコードから測定する、もしくはアプリケーションにプロファイラをアタッチする方法があります(JVMヒープの詳細は、”Understanding Java Memory Model”をご覧ください)。

JVM Instrumentation
https://metrics.dropwizard.io/3.1.0/manual/jvm/
Understanding Java Memory Model
https://medium.com/platform-engineer/understanding-java-memory-model-1d0863f6d973

3. Take a heap snapshot (dump) before the test.(テスト前にヒープダンプ [スナップショット]を取得)

jmapコマンドを使ってデバッグ前にスナップショットファイルを作成します。

jmap -dump:format=b,file=heap.bin <pid>
4. Perform some resource heavy operations on your app.(アプリでリソースヘビーな操作を実行)

いくつかの高負荷シナリオでアプリケーションを動かします。例えば、10個のデータセット(各20GB)をアップロードします。

5. Check the application’s JVM memory graph after full garbage collection.(Full GC後のアプリケーションのJVMメモリのグラフをチェック)

何回かのfull GC後にold genスペースが増え続けていることがわかった場合、それはメモリリークの明確な兆候です。下のグラフでは、青い色がyoung genスペース、オレンジ色がold genスペースを表しています。何回かのGC後、オブジェクトはyoung gen(青)からold gen(オレンジ)へと移動し、決して解放されていないことがわかります。これはメモリリークの明確なサインです。

Image for post

YourkitのようなほとんどのJavaプロファイラでは、アプリケーションの実行中にfull GCを引き起こすために非常に便利な強制GC機能を提供しています。アプリケーションにプロファイラをアタッチすることができない場合は、GCログをチェックしてfull GCがいつ発生するかを確認し、アプリケーションのJVMメトリックをチェックして、その時点でのyoung/old genの使用率を取得できます。

6. Take second heap snapshot and start to analyze. (2回目のヒープダンプを取得して分析開始)

2回目のヒープスナップショットを取得し、プロファイラで調べて、full GC後も参照されている、最も増えたオブジェクトを見つけます。プロファイラのGeneration View(世代ビュー)またはReachability View(到達性ビュー)から探し始めます(下の例を参照)。分からない場合は、2回目のヒープダンプとステップ3で取得した最初のヒープダンプを比較してみてください。

Image for post
7. Find suspect objects in code.(コードで怪しいオブジェクトを発見)

プロファイラで大きなオブジェクトそれぞれのincoming referenceをチェックし、コード解析を行ってコード・ベース内のこれらの参照を見つけます。それらが根本的な原因である可能性が高いです。

下図のプロファイラのスクリーンショットでは、Object explorerの画面から、Stringオブジェクトの大部分がJava arrayListから参照されており、36 MB のメモリを消費していることがわかります。これはリークの可能性があることを示しており、ソースコードをチェックする必要があります。

Image for post
8. Break the reference and verify (参照を破壊して検証する)

コード内の疑わしいオブジェクトの参照を解除し、手順3から7を繰り返して、アプリケーションのメモリグラフが平坦になることを確認してください。もしそうであれば、根本的な原因が見つかったということです。通常、修正は簡単ですが、コードのリファクタリングを必要とするより複雑なケースのために、次回の投稿 “Fix Memory Issues in Your Java Apps “のセクション “Mitigate Memory Pressure” をチェックしてください。

大事なことを言い忘れていましたが、profiler worship(プロファイラ崇拝)という考え方は止めておきたいと考えています。確かにプロファイラは素晴らしい助っ人ですが、それが全てではありません。プロファイラを使う主要な理由は整理された形でJVMの内部を公開するためです。大抵の場合、基本的な(無料の)プロファイラで十分に賄えます。メモリのトラブルシューティングの本当の課題は、プロファイラが公開している、問題の原因となっているアプリケーション・コードをどのようにして見つけるかということです。この問題を解決する唯一の方法は、アプリケーションのコードベースを理解し、Java言語を十分に理解することです。

トラブルシューティング・ツールに精通し、一般的なトラブルシューティングの方法論を知っていれば、メモリ・リークの根本的な原因を見つけることはそれほど難しくないはずです。しかし、根本原因を発見してから、アプリケーションをどのように修正するのでしょうか?次の投稿“Fix Memory Issues in Your Java Apps”でその件について述べていきます。

Reference

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Google フォト

Google アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中