原文はこちら。
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/
このブログ記事では、以下の方法を学びます。
- Javaのメモリリークの理解
- メモリリークのトラブルシューティング
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.”
A Guide to Java Profilers – https://www.baeldung.com/java-profilers
Java Profilerは、Javaバイトコードの構成と操作をJVMレベルで監視するツールです。これらのコード構成と操作には、オブジェクトの作成、反復実行(再帰的呼び出しを含む)、メソッドの実行、スレッドの実行、およびガベージコレクションが含まれます。
実際には、プロファイラは、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(オレンジ)へと移動し、決して解放されていないことがわかります。これはメモリリークの明確なサインです。

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で取得した最初のヒープダンプを比較してみてください。

7. Find suspect objects in code.(コードで怪しいオブジェクトを発見)
プロファイラで大きなオブジェクトそれぞれのincoming referenceをチェックし、コード解析を行ってコード・ベース内のこれらの参照を見つけます。それらが根本的な原因である可能性が高いです。
下図のプロファイラのスクリーンショットでは、Object explorerの画面から、Stringオブジェクトの大部分がJava arrayList
から参照されており、36 MB のメモリを消費していることがわかります。これはリークの可能性があることを示しており、ソースコードをチェックする必要があります。

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
- 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/ - Understanding Memory Leaks in Java (Baeldung)
https://www.baeldung.com/java-memory-leaks - A Guide to Java Profilers (Baeldung)
https://www.baeldung.com/java-profilers - Understanding Java Memory Model
https://medium.com/platform-engineer/understanding-java-memory-model-1d0863f6d973