3 メモリー・リークのトラブルシューティング
アプリケーションの実行時間が長くなった場合や、オペレーティング・システムの実行速度が低下したように思われる場合、これはメモリー・リークの兆候である可能性があります。つまり、仮想メモリーが割り当てられているけれども、それが不要になったときに返されません。最終的には、アプリケーションまたはシステムがメモリーを使い果たし、アプリケーションが異常終了します。
この章の構成は、次のとおりです。
java.lang.OutOfMemoryErrorエラー
メモリー・リークの一般的な兆候の1つは、java.lang.OutOfMemoryError
エラーです。このエラーは、ガベージ・コレクタによって新しいオブジェクトを格納するための空間を確保することも、ヒープをこれ以上拡張することもできないことを示します。また、このエラーは、Javaクラスのロードをサポートするための十分なネイティブ・メモリーがないときにスローされる場合もあります。ガベージ・コレクションの実行に過剰な時間が消費され、メモリーがほとんど解放されていない場合、まれにこのエラーがスローされることがあります。
java.lang.OutOfMemoryError
エラーは、ネイティブ割当てを満たすことができないとき(たとえば、スワップ空間が少ない場合)に、ネイティブ・ライブラリ・コードによってスローされることもあります。
java.lang.OutOfMemoryError
エラーがスローされると、スタック・トレースが出力されます。
java.lang.OutOfMemoryError
エラーを診断する最初のステップは、その原因を突き止めることです。スローされた理由が、Javaヒープがいっぱいであるからなのか、それともネイティブ・ヒープがいっぱいであるからなのかを調べます。原因の特定に役立つよう、詳細メッセージが例外テキストに追加されます(次の例を参照):
- 詳細メッセージ: Javaヒープ領域
-
原因: 詳細メッセージ「Javaヒープ領域」は、Javaヒープ内でオブジェクトを割り当てることができなかったことを示しています。このエラーは、必ずしもメモリー・リークを意味しません。この問題は、指定されたヒープ・サイズ(指定されていない場合はデフォルト・サイズ)がアプリケーションにとって十分でないという、単純な構成の問題である可能性があります。Javaヒープ領域の初期および最大サイズは、
–Xms
および–Xmx
オプションを使用して構成できます。それ以外の場合、特に長期にわたって稼働するアプリケーションでは、このメッセージはアプリケーションが誤ってオブジェクトへの参照を保持しているため、オブジェクトのガベージ・コレクションができないことを示している可能性があります。これは、Java言語ではメモリー・リークに相当します。ノート:
アプリケーションによって呼び出されたAPIが誤ってオブジェクト参照を保持している可能性もあります。このエラーのもう1つの潜在的な原因は、ファイナライザを過度に使用するアプリケーションで発生します。クラスに
finalize
メソッドがある場合、そのタイプのオブジェクトの空間はガベージ・コレクション時に再利用されません。かわりに、ガベージ・コレクション後にファイナライズ用のキューにオブジェクトが入れられ、後でファイナライズが実行されます。JavaランタイムのOracle実装では、ファイナライザはファイナライズ・キューを提供するデーモン・スレッドによって実行されます。スレッドがファイナライズ・キューを処理しきれなかった場合は、Javaヒープがいっぱいになる可能性があり、この種類のjava.lang.OutOfMemoryError
エラーがスローされます。この状況を発生させるシナリオの1つは、アプリケーションが優先度の高いスレッドを作成しているため、ファイナライザ・スレッドがファイナライズ・キューを処理する速度よりキューの増加速度の方が速くなっている場合です。 - 詳細メッセージ: GCオーバーヘッド制限を超えました
-
原因: 詳細メッセージ「GCオーバーヘッド制限を超えました」は、ガベージ・コレクタ(GC)がほとんどの時間、実行されているため、Javaアプリケーションの処理がほとんど進んでいないことを示しています。ガベージ・コレクション後、Javaアプリケーションでガベージ・コレクションの実行に約98%を超える時間が費やされ、ヒープのリカバリが2%未満であり、その状態が直近5回(コンパイル時間定数)の連続するガベージ・コレクションで続いている場合に、
java.lang.OutOfMemoryError
エラーがスローされます。このエラーは通常、Javaヒープに新しい割当て用の空き領域がわずかしか残っておらず、ライブ・データの容量をほとんど格納できないためにスローされます。 - 詳細メッセージ: 要求された配列サイズがVM制限を超えています
-
原因: リクエストされた配列サイズはVMの制限を超えていますという詳細メッセージは、使用可能なヒープ・サイズに関係なく、アプリケーション(またはアプリケーションが使用するAPI)がVM実装の制限より大きいサイズの配列を割り当てようとしたことを示します。
- 詳細メッセージ: メタスペース
-
原因: Javaクラス・メタデータ(Javaクラスの仮想マシン内部表現)がネイティブ・メモリー(ここでは、メタスペースと呼ばれる)に割り当てられています。クラス・メタデータのメタスペースが枯渇すると、詳細メッセージ「メタスペース」とともに
java.lang.OutOfMemoryError
エラーがスローされます。クラス・メタデータに使用できるメタスペース容量は、コマンド行に指定するパラメータMaxMetaSpaceSize
により制限されます。クラス・メタデータに必要なネイティブ・メモリー容量がMaxMetaSpaceSize
を超過すると、詳細メッセージ「メタスペース」とともにjava.lang.OutOfMemoryError
エラーがスローされます。 - 詳細メッセージ: reasonのためにsizeバイトを要求します。スワップ領域が不足していますか。
-
原因: 詳細メッセージ「reasonのためにsizeバイトを要求します。スワップ領域が不足していますか。」は、
java.lang.OutOfMemoryError
エラーであるように見えます。しかし、ネイティブ・ヒープからの割当てが失敗し、ネイティブ・ヒープが枯渇寸前になっている可能性があるときに、Javaはこの見かけ上のエラーを報告します。このメッセージは、失敗した要求のサイズ(バイト数)とメモリー要求の理由を示しています。通常、reasonは割当ての失敗を報告するソース・モジュールの名前になりますが、場合によっては実際の理由を示すこともあります。 - 詳細メッセージ: 圧縮クラス領域
-
原因: 64ビット・プラットフォーム上で、クラス・メタデータへのポインタが32ビットのオフセットで表現されている可能性があります(
UseCompressedOops
を使用)。これはコマンド行フラグUseCompressedClassPointers
で制御されます(デフォルトではtrue
)。UseCompressedClassPointers
がtrue
の場合、クラス・メタデータに使用できる領域量はCompressedClassSpaceSize
の容量で固定されます。UseCompressedClassPointers
に必要な領域がCompressedClassSpaceSize
を超える場合、詳細メッセージ「圧縮クラス領域」とともにjava.lang.OutOfMemoryError
エラーがスローされます。 - 詳細メッセージ: reason stack_trace (ネイティブ・メソッド)
-
原因: この詳細メッセージは、ネイティブ・メソッドで割当て失敗が発生したことを示しています。これと前のメッセージとの違いは、割当ての失敗がJVM自体ではなく、Java Native Interface (JNI)またはネイティブ・メソッドで検出されたことです。
メモリー・リークの検出
java.lang.OutOfMemoryError
エラーは、Javaアプリケーションのメモリー・リークの兆候である可能性があります。また、Javaヒープ、メタスペースまたは圧縮クラス領域のいずれかが、その特定のメモリー・プールに対するアプリケーションのメモリー要件よりも小さいサイズであることを示している可能性もあります。アプリケーションでメモリー・リークが発生していると想定する前に、java.lang.OutOfMemoryError
エラーが発生しているメモリー・プールのサイズが適切であることを確認します。Javaヒープのサイズは、–Xmx
および–Xms
コマンド行オプションを使用して設定でき、メタスペースの最大サイズおよび初期サイズは、MaxMetaspaceSize
およびMetaspaceSize
を使用して構成できます。同様に、圧縮クラス領域のサイズは、CompressedClassSpaceSize
オプションを使用して設定できます。
メモリー・リークは、特に低速のものは検出が非常に困難な場合が多くあります。メモリー・リークは、Javaオブジェクトまたクラスへの参照がアプリケーションにより誤って保持され、ガベージ・コレクションによって解放されない場合に発生します。誤って保持されているこれらのオブジェクトまたはクラスは、時間の経過とともにメモリー内で増大し、最終的にJavaヒープまたはメタスペース全体をいっぱいにしてしまい、頻繁にガベージ・コレクションが発生して、最終的にはプロセスがjava.lang.OutOfMemoryError
エラーで終了します。
メモリー・リークを検出するには、アプリケーションのライブ・セット(つまり、完全なガベージ・コレクション後に使用されるJavaヒープ領域またはメタスペースの量)をモニターすることが重要です。アプリケーションが安定した状態になり、安定した負荷がかかった後、ライブ・セットが時間の経過とともに増加する場合は、メモリー・リークの強い兆候である可能性があります。アプリケーションのライブ・セットおよびメモリー使用量は、JConsoleおよびJDK Mission Controlを使用してモニターできます。メモリー使用量の情報は、ガベージ・コレクション・ログから抽出することもできます。
エラーの詳細メッセージがネイティブ・ヒープの枯渇を示している場合は、アプリケーションでネイティブ・メモリー・リークが発生している可能性があります。ネイティブ・メモリー・リークを確認するには、pmapやPerfMonなどのネイティブ・ツールを使用し、定期的に収集される出力を比較して、プロセスの新しく割り当てられた、または増大しているメモリー・セクションを判断します。
JConsole
JConsoleは、Javaアプリケーションのリソースをモニターするための優れたツールです。特に、Javaヒープ、メタスペース、圧縮クラス領域、CodeHeapの世代など、アプリケーションの様々なメモリー・プールの使用量をモニターするのに役立ちます。
次のスクリーンショットでは、プログラムの例として、JConsoleはヒープ・メモリーおよび古い世代の使用量が一定期間にわたって着実に増加していることを示しています。完全ガベージ・コレクションを数回行った後でも、このようにメモリー使用量が着実に増加している場合は、メモリー・リークが発生していることを示しています。
図3-1 JConsoleのヒープ・メモリー
図3-2 JConsoleの古い世代
JDK Mission Control
JDK Mission Control (JMC)を使用して、メモリー・リークを早期に検出し、java.lang.OutOfMemoryError
エラーを防止できます。
低速のメモリー・リークの検出は難しい場合があります。一般的な症状は、頻繁なガベージ・コレクションにより実行に時間がかかり、その後、アプリケーションが低速になります。最終的に、java.lang.OutOfMemoryError
エラーが発生する可能性があります。ただし、Javaフライト記録を分析することで、このような問題が発生する前であっても、メモリー・リークを早期に検出できます。
アプリケーションのライブ・セットの経時的な増加がないか確認します。ライブ・セットとは、すべての到達不能オブジェクトを収集する完全ガベージ・コレクションの後に使用されているJavaヒープ容量です。ライブ・セットを検査するには、JMCを起動し、Java管理コンソール(JMX)を使用してJVMに接続します。「MBeanブラウザ」タブを開き、com.sun.management
の下のGarbageCollectorAggregator
MBeanを探します。
JMCを起動し、1時間分の一定時間の記録(プロファイリング記録)を開始します。フライト記録を開始する前に、「メモリー・リーク検出」設定から「オブジェクト・タイプ+割当てスタック・トレース+ GCルートへのパス」オプションが選択されていることを確認してください。
記録が完了すると、JMCで記録ファイル(.jfr
)が開きます。「自動分析結果」ページを参照してください。メモリー・リークを検出するには、ページの「ライブ・オブジェクト」セクションにフォーカスを置きます。次に、ヒープ・サイズの問題を示す記録の例を示します:
「ヒープ・ライブ・セットの傾向」セクションで、ヒープのライブ・セットが急激に増加し、参照ツリーの分析でリーク候補が検出されたことがわかります。
さらに分析するには、「Javaアプリケーション」ページを開き、「メモリー」ページをクリックします。次に、メモリー・リークの問題を示す記録のサンプル図を示します。
グラフから、メモリー使用量が徐々に増加し、メモリー・リークの問題が示されていることを確認できます。
ガベージ・コレクションのログ
メモリー使用量の情報は、GCログを使用して抽出することもできます。アプリケーションが古い世代またはメタスペースで領域を再利用しようとして完全ガベージ・コレクションを何度か実行したことがGCログに示されているが、大きなメリットがなかった場合は、アプリケーションでメモリー・リークの問題が発生している可能性があります。
–Xlog
コマンド行Javaオプションを使用して収集できます。次に例を示します。-Xlog:gc*,gc+phases=debug:gc.log
これにより、少なくともgc
でタグ付けされたメッセージはinfo
レベルで、正確にgc
とphases
でタグ付けされたメッセージはdebug
レベルで、gc.log
というファイルに記録されます。
-Xlog:gc*
で収集されたGCログからの抜粋を示します。[4.344s][info][gc,start ] GC(46) Pause Full (Ergonomics)
[4.344s][info][gc,phases,start] GC(46) Marking Phase
[4.402s][info][gc,phases ] GC(46) Marking Phase 57.896ms
[4.402s][info][gc,phases,start] GC(46) Summary Phase
[4.402s][info][gc,phases ] GC(46) Summary Phase 0.023ms
[4.402s][info][gc,phases,start] GC(46) Adjust Roots
[4.402s][info][gc,phases ] GC(46) Adjust Roots 0.108ms
[4.402s][info][gc,phases,start] GC(46) Compaction Phase
[4.435s][info][gc,phases ] GC(46) Compaction Phase 33.721ms
[4.436s][info][gc,phases,start] GC(46) Post Compact
[4.436s][info][gc,phases ] GC(46) Post Compact 0.073ms
[4.436s][info][gc,heap ] GC(46) PSYoungGen: 12799K(14848K)->12799K(14848K) Eden: 12799K(12800K)->12799K(12800K) From: 0K(2048K)->0K(2048K)
[4.436s][info][gc,heap ] GC(46) ParOldGen: 34072K(34304K)->34072K(34304K)
[4.436s][info][gc,metaspace ] GC(46) Metaspace: 149K(384K)->149K(384K) NonClass: 145K(256K)->145K(256K) Class: 3K(128K)->3K(128K)
[4.436s][info][gc ] GC(46) Pause Full (Ergonomics) 45M->45M(48M) 92.086ms
[4.436s][info][gc,cpu ] GC(46) User=0.15s Sys=0.01s Real=0.10s
この例では、Javaヒープは48Mにサイズ設定され、完全GCはどの領域も再利用できませんでした。古い世代は完全にいっぱいであり、完全GCがあまり役に立たなかったことは明らかです。これは、ヒープのサイズがアプリケーションのヒープ要件より小さいか、メモリー・リークが発生していることを示しています。Javaメモリー・リークの診断
Javaソース・コード内のリークの診断は難しい可能性があります。通常、アプリケーションの非常に詳しい知識が必要です。さらに、プロセスが反復し、冗長なことがよくあります。この項では、Javaソース・コード内のメモリー・リークを診断するために使用できるツールについて説明します。
診断データ
この項では、メモリー・リークのトラブルシューティングに使用できる診断データについて説明します。
ヒープ・ダンプ
ヒープ・ダンプは、メモリー・リークのトラブルシューティングに最も重要なデータです。ヒープ・ダンプは、jcmd
、jmap
、JConsoleツールおよび-XX:+HeapDumpOnOutOfMemoryError
Javaオプションを使用して収集できます
Javaフライト記録
図3-5 フライト記録テンプレート・マネージャ
.jfc
ファイルを手動で編集し、heap-statistics-enabled
をtrue
に設定して有効にすることもできます。<event path="vm/gc/detailed/object_count">
<setting name="enabled" control="heap-statistics-enabled">true</setting>
<setting name="period">everyChunk</setting>
</event>
その後、次のいずれかの方法でフライト記録を作成できます:
クラス・ローダーの統計
クラス・ローダーおよびそれによってロードされるクラスの数に関する情報は、メタスペースおよび圧縮クラス領域に関連するメモリー・リークを診断する際に非常に役立ちます。
jcmd
およびjmap
ツールを使用して収集できます:
jcmd <process id/main name> VM.classloader_stats
jmap –clstats <process id>
次に、jmap
によって生成された出力の例を示します。
jmap -clstats 15557
ClassLoader Parent CLD* Classes ChunkSz BlockSz Type
0x0000000800c40ac8 0x0000000800085938 0x00007ff3a18787e0 1 768 184 java.net.URLClassLoader
0x0000000800c40ac8 0x0000000800085938 0x00007ff39f652f90 1 768 184 java.net.URLClassLoader
0x0000000800c40ac8 0x0000000800085938 0x00007ff39f499620 1 768 184 java.net.URLClassLoader
0x0000000800c40ac8 0x0000000800085938 0x00007ff3a194e3a0 1 768 184 java.net.URLClassLoader
0x0000000800c40ac8 0x0000000800085938 0x00007ff39f5aaad0 1 768 184 java.net.URLClassLoader
0x0000000800c40ac8 0x0000000800085938 0x00007ff3a1823d20 1 768 184 java.net.URLClassLoader
0x0000000800c40ac8 0x0000000800085938 0x00007ff3a194cab0 1 768 184 java.net.URLClassLoader
0x0000000800c40ac8 0x0000000800085938 0x00007ff3a1883190 1 768 184 java.net.URLClassLoader
0x0000000800c40ac8 0x0000000800085938 0x00007ff3a191b9c0 1 768 184 java.net.URLClassLoader
0x0000000800c40ac8 0x0000000800085938 0x00007ff3a1914810 1 768 184 java.net.URLClassLoader
0x0000000800c40ac8 0x0000000800085938 0x00007ff3a181c050 1 768 184 java.net.URLClassLoader
0x0000000800c40ac8 0x0000000800085938 0x00007ff39f5c57c0 1 768 184 java.net.URLClassLoader
0x0000000800c40ac8 0x0000000800085938 0x00007ff39f6774d0 1 768 184 java.net.URLClassLoader
0x0000000800c40ac8 0x0000000800085938 0x00007ff3a18574b0 1 768 184 java.net.URLClassLoader
0x0000000800c40ac8 0x0000000800085938 0x00007ff3a1803500 1 768 184 java.net.URLClassLoader
…
Total = 1105 1757 1404032 308858
ChunkSz: Total size of all allocated metaspace chunks
BlockSz: Total size of all allocated metaspace blocks (each chunk has several blocks)
分析ツール
この項では、メモリー・リークの診断に使用できる分析ツール(前述の診断データを分析できるツールを含む)について説明します。
ヒープ・ダンプ分析ツール
ヒープ・ダンプ分析に使用できる多くのサードパーティ・ツールがあります。メモリー・デバッグ機能を備えた商用ツールの例として、Eclipse Memory Analyzer Tool (MAT)とYourKitの2つがあります。他にも数多くありますが、推奨される特定の製品はありません。
JDK Mission Control (JMC)
フライト・レコーダは、JavaランタイムおよびJavaランタイムで実行されているJavaアプリケーションの詳細情報を記録します。
この項では、JMCでフライト記録を分析してメモリー・リークをデバッグする方法について説明します。
Javaフライト記録を使用して、リーク・オブジェクトを識別できます。リーク・クラスを検索するには、「メモリー」ページを開き、「ライブ・オブジェクト」ページをクリックします。次に、リーク・クラスを示す記録のサンプル図を示します。
追跡されているライブ・オブジェクトのほとんどがLeak$DemoThread
によって保持されており、このオブジェクトはリークされたchar[]
クラスを保持していることがわかります。詳細な分析は、存続したオブジェクトのサンプリングを含む「結果」タブの古いオブジェクト・サンプル
・イベントを参照してください。このイベントには、割当て時間、割当てスタック・トレース、およびGCルートに戻るパスが含まれます。
リークの可能性があるクラスが特定されたら、「JVM内部」ページの「TLAB割当て」ページで、オブジェクトが割り当てられた場所のサンプルを調べます。次に、TLAB割当てを示す記録のサンプルを示します。
割り当てられているクラス・サンプルをチェックします。スロー・リークの場合は、このオブジェクトの割当てが少なく、サンプルがないこともあります。また、特定の割当てサイトのみがリークの原因となっている可能性もあります。コードに必要な変更を加えて、リーク・クラスを修正できます。
jfrツール
Javaフライト・レコーダ(JFR)は、JavaランタイムおよびJavaランタイムで実行されているJavaアプリケーションの詳細情報を記録します。この情報はメモリー・リークを特定するために使用できます。
メモリー・リークを検出するには、JFRがリークの発生時点で実行されている必要があります。JFRのオーバーヘッドは、1%未満と非常に低く、常に本番環境で安全に稼働するように設計されています。
次の例に示すように、アプリケーションがjava
コマンドを使用して起動されたときに記録を開始します。
$ java -XX:StartFlightRecording
JVMがメモリー不足になってjava.lang.OutOfMemoryError
エラーにより終了した場合、接頭辞hs_oom_pid
を持つ記録は多くの場合、JVMが起動されたディレクトリに書き込まれますが、常にそうではありません。記録を取得する別の方法は、次の例に示すように、jcmd
ツールを使用してアプリケーションでメモリーが不足する前に記録をダンプすることです。
$ jcmd pid JFR.dump filename=recording.jfr path-to-gc-roots=true
記録がある場合は、java-home/bin
ディレクトリにあるjfr
ツールを使用して、メモリー・リークの可能性に関する情報を含む古いオブジェクト・サンプル・イベントを出力します。次の例は、pid 16276のアプリケーションに対するコマンドと記録からの出力例を示しています。
jfr print --events OldObjectSample pid16276.jfr
...
jdk.OldObjectSample {
startTime = 18:32:52.192
duration = 5.317 s
allocationTime = 18:31:38.213
objectAge = 74.0 s
lastKnownHeapUsage = 63.9 MB
object = [
java.util.HashMap$Node
[15052855] : java.util.HashMap$Node[33554432]
table : java.util.HashMap Size: 15000000
map : java.util.HashSet
users : java.lang.Class Class Name: Application
]
arrayElements = N/A
root = {
description = "Thread Name: main"
system = "Threads"
type = "Stack Variable"
}
eventThread = "main" (javaThreadId = 1)
}
...
jdk.OldObjectSample {
startTime = 18:32:52.192
duration = 5.317 s
allocationTime = 18:31:38.266
objectAge = 74.0 s
lastKnownHeapUsage = 84.4 MB
object = [
java.util.HashMap$Node
[8776975] : java.util.HashMap$Node[33554432]
table : java.util.HashMap Size: 15000000
map : java.util.HashSet
users : java.lang.Class Class Name: Application
]
arrayElements = N/A
root = {
description = "Thread Name: main"
system = "Threads"
type = "Stack Variable"
}
eventThread = "main" (javaThreadId = 1)
}
...
jdk.OldObjectSample {
startTime = 18:32:52.192
duration = 5.317 s
allocationTime = 18:31:38.540
objectAge = 73.7 s
lastKnownHeapUsage = 121.7 MB
object = [
java.util.HashMap$Node
[393162] : java.util.HashMap$Node[33554432]
table : java.util.HashMap Size: 15000000
map : java.util.HashSet
users : java.lang.Class Class Name: Application
]
arrayElements = N/A
root = {
description = "Thread Name: main"
system = "Threads"
type = "Stack Variable"
}
eventThread = "main" (javaThreadId = 1)
}
...
メモリー・リークの可能性を特定するには、記録内の次の要素を確認します。
-
最初に、古いオブジェクト・サンプル・イベントの
lastKnownHeapUsage
要素が、一定期間にわたり(例では、最初のイベントの63.9 MBから最後のイベントの121.7 MBに)増加していることに注意してください。この増加は、メモリー・リークが存在することを示しています。ほとんどのアプリケーションは、起動時にオブジェクトを割り当て、その後定期的にガベージ・コレクションされる一時オブジェクトを割り当てます。何らかの理由でガベージ・コレクションされないオブジェクトは、時間とともに蓄積され、lastKnownHeapUsage
の値が増加します。 -
次に、
allocationTime
要素を参照して、オブジェクトがいつ割り当てられたかを確認します。通常、起動時に割り当てられるオブジェクトはメモリー・リークではなく、ダンプの取得時の近くで割り当てられるオブジェクトでもありません。objectAge
要素は、オブジェクトが存続している期間を示します。startTime
要素とduration
要素は、メモリー・リークが発生した時点ではなく、OldObject
イベントが発行された時点と、そのデータの収集にかかった時間に関連しています。この情報は無視してかまいません。 -
次に、
object
要素を調べてメモリー・リーク候補を確認します。この例では、java.util.HashMap$Node型のオブジェクトです。これは、java.util.HashMapクラスのtable
フィールドによって保持されます。このフィールドは、Applicationクラスのusers
フィールドによって保持されるjava.util.HashSetによって保持されます。 -
root
要素には、GCルートに関する情報が含まれます。この例では、Applicationクラスは、メイン・スレッドのスタック変数によって保持されています。eventThread
要素は、オブジェクトを割り当てたスレッドに関する情報を提供します。
-XX:StartFlightRecording:settings=profile
オプションを指定してアプリケーションが起動された場合、記録には、次の例に示すようにオブジェクトが割り当てられた場所からのスタック・トレースも含まれます。
stackTrace = [
java.util.HashMap.newNode(int, Object, Object, HashMap$Node) line: 1885
java.util.HashMap.putVal(int, Object, Object, boolean, boolean) line: 631
java.util.HashMap.put(Object, Object) line: 612
java.util.HashSet.add(Object) line: 220
Application.storeUser(String, String) line: 53
Application.validate(String, String) line: 48
Application.login(String, String) line: 44
Application.main(String[]) line: 30
]
この例では、storeUser(String, String)
メソッドが呼び出されたときに、オブジェクトがHashSet
に格納されていたことがわかります。これにより、メモリー・リークの原因は、ユーザーがログアウトしたときにHashSet
から削除されなかったオブジェクトである可能性があると考えられます。
一定の割当て集中型アプリケーションでのオーバーヘッドのため、常に-XX:StartFlightRecording:settings=profile
オプションを指定してすべてのアプリケーションを実行することはお薦めできませんが、通常、デバッグ時であれば問題ありません。通常、オーバーヘッドは2%未満です。
path-to-gc-roots=true
を設定すると、完全ガベージ・コレクションと同様にオーバーヘッドが発生しますが、GCルートへの参照チェーンも提供されます。これは通常、メモリー・リークの原因を確認するために十分な情報です。
NetBeans Profiler
NetBeans Profilerを使用すると、メモリー・リークの場所をとても簡単に特定できます。商用のメモリー・リーク・デバッグ・ツールは、大規模なアプリケーション内のリークを特定するのに長い時間がかかる場合があります。しかし、NetBeans Profilerは、このようなオブジェクトが一般的に示すメモリーの割り当てと再利用のパターンを使用します。このプロセスには、メモリー再利用の欠落も含まれます。このプロファイラは、これらのオブジェクトがどこで割り当てられたかをチェックできます(リークの根本原因を特定するには、通常はこれで十分です)。
NetBeans IDEでのJavaアプリケーションのプロファイリングの概要に関するサイトを参照してください。
ネイティブ・メモリー・リークの診断
複数の手法を使用してネイティブ・コードのメモリー・リークを検出し、切り分けることができます。一般に、すべてのプラットフォームに対応できる理想的な解決方法はありません。
ネイティブ・コードを診断するためのいくつかの手法を次に示します。
すべてのメモリー割当ておよび解放呼出しの追跡
非常によく使われる方法は、ネイティブ割当てのすべての割当て呼出しと解放呼出しを追跡することです。これは、かなり簡単なプロセスになる場合と、非常に高度なプロセスになる場合があります。ネイティブ・ヒープの割り当てとそのメモリーの使用の追跡をめぐっては、長年にわたって多くの製品が構築されています。
IBM Rational Purifyのようなツールを使用すると、ネイティブ・コードの通常の状況でこれらのリークを検出したり、初期化されていないメモリーへの割当てや解放されたメモリーへのアクセスを表すネイティブ・ヒープ・メモリーへのアクセスを検出したりできます。
これらのタイプのツールのすべてがネイティブ・コードを使用するJavaアプリケーションで機能するわけではなく、これらのツールは通常はプラットフォーム固有です。JVMはコードを実行時に動的に作成するため、これらのツールはコードを誤って解釈したり、まったく動作しなかったり、誤った情報を与えたりする可能性があります。ツールのベンダーに問い合せて、ツールのバージョンが、使用しているJVMのバージョンで動作することを確認してください。
多数の簡単で移植可能なネイティブ・メモリー・リークの検出例は、SourceForgeに関するサイトを参照してください。ほとんどのライブラリやツールは、アプリケーションのソースを再コンパイルまたは編集して、割当て関数にラッパー関数を被せることができることを前提としています。より強力なツールでは、これらの割当て関数に動的に介入することで、アプリケーションを変更せずに実行できます。
ネイティブ・メモリー・リークは、JVMによって内部的に実行されるか、JVMの外部から実行されるネイティブ割当てによって発生します。次の2つの項では、これらの両方のメモリー・リークを診断する方法について詳しく説明します。
JVMによって実行される割当てのネイティブ・メモリー・リーク
JVMには、ネイティブ・メモリー・トラッキング(NMT)と呼ばれる強力なツールがあり、JVMによって内部的に実行されるネイティブ・メモリー割当てを追跡します。このツールは、JNIコードなど、JVMの外部で割り当てられたネイティブ・メモリーを追跡できません。
- Javaオプション
NativeMemoryTracking
を使用して、モニターするプロセスでNMTを有効にします。トラッキングの出力レベルは、次に示すようにsummary
またはdetail
レベルに設定できます:-XX:NativeMemoryTracking=summary -XX:NativeMemoryTracking=detail
- その後、jcmdツールを使用してNMT対応プロセスに接続し、ネイティブ・メモリー使用量の詳細を取得できます。また、メモリー使用量のベースラインを収集し、そのベースラインに対する使用量の差を収集することもできます。
jcmd <process id/main class> VM.native_memory jcmd <process id/main class> VM.native_memory baseline jcmd <process id/main class> VM.native_memory detail.diff/summary.diff
ノート:
NMTを有効にすると、パフォーマンスが約5~10パーセント低下する可能性があります。したがって、本番システムで有効にする際には慎重に行う必要があります。また、NMTで使用されるネイティブ・メモリーは、ツール自体によって追跡されます。図3-8 JConsole圧縮クラス領域
summary.diff
の出力を収集すると、ロード済クラス数の増加に対応して、クラス領域の使用量が大幅に増加していることがわかります。 bash-3.2$ jcmd 39057 VM.native_memory summary.diff
39057:
Native Memory Tracking:
Total: reserved=5761678KB +52943KB, committed=472350KB +104143KB
- Java Heap (reserved=4194304KB, committed=163328KB +7680KB)
(mmap: reserved=4194304KB, committed=163328KB +7680KB)
- Class (reserved=1118333KB +47579KB, committed=117949KB +89963KB)
(classes #68532 +58527)
(malloc=8317KB +2523KB #5461 +3371)
(mmap: reserved=1110016KB +45056KB, committed=109632KB +87440KB)
- Thread (reserved=21594KB -2057KB, committed=21594KB -2057KB)
(thread #22 -2)
(stack: reserved=21504KB -2048KB, committed=21504KB -2048KB)
(malloc=65KB -6KB #111 -10)
(arena=25KB -2 #42 -4)
- Code (reserved=250400KB +244KB, committed=5612KB +1348KB)
(malloc=800KB +244KB #1498 +234)
(mmap: reserved=249600KB, committed=4812KB +1104KB)
- GC (reserved=159039KB +18KB, committed=145859KB +50KB)
(malloc=5795KB +18KB #856 +590)
(mmap: reserved=153244KB, committed=140064KB +32KB)
- Compiler (reserved=153KB, committed=153KB)
(malloc=22KB #72 -2)
(arena=131KB #3)
- Internal (reserved=13537KB +6949KB, committed=13537KB +6949KB)
(malloc=13505KB +6949KB #70630 +59119)
(mmap: reserved=32KB, committed=32KB)
- Symbol (reserved=2715KB +9KB, committed=2715KB +9KB)
(malloc=1461KB +9KB #702 +29)
(arena=1255KB #1)
- Native Memory Tracking (reserved=1416KB +1031KB, committed=1416KB +1031KB)
(malloc=140KB +34KB #2197 +518)
(tracking overhead=1275KB +997KB)
- Arena Chunk (reserved=186KB -832KB, committed=186KB -832KB)
(malloc=186KB -832KB)
JVM外からのネイティブ・メモリー・リーク
JVM外部で発生するネイティブ・メモリー・リークの場合、プラットフォーム・ネイティブまたはその他のサードパーティ・ツールを使用して、検出およびトラブルシューティングを行うことができます。これは、JVMの外部で実行される割当てによって発生するネイティブ・メモリー・リークのトラブルシューティングに役立つツールのリストです。
- Valgrind
- UNIXプラットフォームとWindowsの両方で使用可能なPurify
- クラッシュ・ダンプまたはコア・ファイルの使用
- Windowsの場合は、Microsoft Docsでデバッグ・サポートを検索してください。Microsoft C++コンパイラには、メモリー割当てを追跡するための追加サポートを自動的に取り込む、
/Md
および/Mdd
コンパイラ・オプションがあります。ユーザー・モード・ダンプ・ヒープ(UMDH)は、メモリー割当ての追跡にも役立ちます。 - Linuxシステムには、割り当て追跡を扱う際に役立つ、
mtrace
やlibnjamd
などのツールがあります。
次の項では、これらのツールのうち2つについて詳しく説明します。
Valgrind
$ valgrind --leak-check=full --show-leak-kinds=all --suppressions=suppression_file --log-file=valgrind_with_suppression.log -v java <Java Class>
抑制ファイルは、--log-file
オプションでValgrindに提供することで、JVM内部割当て(Javaヒープ割当てなど)を潜在的なメモリー・リークとみなさないようにすることができます。そうしないと、詳細出力を介して解析し、関連するリーク・レポートを手動で検索することが非常に困難になります。
{
name
Memcheck:Leak
fun:*alloc
...
obj:/opt/java/jdk16/jre/lib/amd64/server/libjvm.so
...
}
==5200== 88 bytes in 1 blocks are still reachable in loss record 461 of 18,861
==5200== at 0x4C2FB55: calloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==5200== by 0x7DCB156: Java_java_util_zip_Deflater_init (in /opt/jdk/ /jre/lib/amd64/libzip.so)
==5200== by 0x80F54FC: ???
==5200== by 0x8105F87: ???
==5200== by 0xFFFFFFFE: ???
==5200== by 0xEC67F74F: ???
==5200== by 0xC241B03F: ???
==5200== by 0xEC67D767: ???
==5200== by 0x413F96F: ???
==5200== by 0x8101E7B: ???
==5200==
==5200== 88 bytes in 1 blocks are still reachable in loss record 462 of 18,861
==5200== at 0x4C2FB55: calloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==5200== by 0x7DCB156: Java_java_util_zip_Deflater_init (in /opt/jdk/jre/lib/amd64/libzip.so)
==5200== by 0x80F54FC: ???
==5200== by 0x8105F87: ???
==5200== by 0xFFFFFFFE: ???
==5200== by 0xEC67FF3F: ???
==5200== by 0xC241B03F: ???
==5200== by 0xEC630EB7: ???
==5200== by 0x413F96F: ???
==5200== by 0x8101E7B: ???
==5200== by 0x41: ???
==5200== by 0x19EAE47F: ???
前述の出力で、Valgrindは、Java_java_util_zip_Deflater_initネイティブ・メソッドからリークしている割当てがあることを正しく報告しています。
ノート:
Valgrindを使用すると、モニター対象アプリケーションのパフォーマンスに悪影響を及ぼす可能性があります。クラッシュ・ダンプまたはコア・ファイル
UNIXプラットフォームでは、pmap
ツールは、時間の経過とともにサイズが変化または増大する可能性のあるメモリー・ブロックを特定するのに役立ちます。増大しているメモリー・ブロックまたはセクションを特定したら、対応するクラッシュ・ダンプまたはコア・ファイルを調べて、それらのメモリー・ブロックを確認できます。これらの場所の値および内容は、有益な手がかりとなり、これらのメモリー・ブロック内の割当てを行うソース・コードに結合し直すのに役立ちます。
$ diff pmap.15767.1 pmap.15767.3
69,70c69,70
< 00007f6d68000000 17036K rw--- [ anon ]
< 00007f6d690a3000 4850K ----- [ anon ]
---
> 00007f6d68000000 63816K rw--- [ anon ]
> 00007f6d690a3000 65536K ----- [ anon ]
前のpmap
の出力から、プロセスの2つのメモリー・スナップショットの間で、00007f6d690a3000のメモリー・ブロックが増大していることがわかります。プロセスから収集されたコア・ファイルを使用して、このメモリー・ブロックの内容を調べることができます。
$ gdb `which java` core.15767
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
...
(gdb) x/100s 0x00007f6d690a3000
0x7f6d690a3000: "mory Leak "
0x7f6d690a300c: "Alert: JNI Memory Leak "
0x7f6d690a3025: "Alert: JNI Memory Leak "
0x7f6d690a303e: "Alert: JNI Memory Leak "
0x7f6d690a3057: "Alert: JNI Memory Leak "
0x7f6d690a3070: "Alert: JNI Memory Leak "
前述の内容は、繰返し文字列「Alert: JNI Memory Leak」がそのメモリー・ブロックに存在することを示しています。関連するメモリー・ブロックで見つかった文字列またはコンテンツをソース・コード内で検索すると、コードの原因につながる可能性があります。この例で使用されているコードは次のとおりです。これらの割当てはJNIコードで実行され、リリースされていません。
JNIEXPORT void JNICALL Java_JNINativeLeak_allocateMemory
(JNIEnv *env, jobject obj, jint size) {
char* bytes = (char*) malloc(size);
printf("Allocated %d bytes at %p \n", size, (void*)bytes);
for (int i=0; i<40; i++) {
strcpy(bytes+i*25, "Alert: JNI Memory Leak ");
}
}
そのため、pmapツールおよびコア・ファイルは、JVMの外部で実行される割当てによって発生するネイティブ・メモリー・リークのルートに到達するのに役立ちます。
JNIライブラリのすべてのメモリー割当ての追跡
JNIライブラリを記述する場合は、簡単なラッパー・アプローチを使用して、ライブラリでメモリー・リークが発生しないことを保証する局所的な方法の作成を検討してください。
次の例の手順は、JNIライブラリ用の簡単な局所的割当て追跡方法です。まず、すべてのソース・ファイルに次の行を定義します。
#include <stdlib.h>
#define malloc(n) debug_malloc(n, __FILE__, __LINE__)
#define free(p) debug_free(p, __FILE__, __LINE__)
これにより、次の例の関数を使用してリークを監視できます。
/* Total bytes allocated */
static int total_allocated;
/* Memory alignment is important */
typedef union { double d; struct {size_t n; char *file; int line;} s; } Site;
void *
debug_malloc(size_t n, char *file, int line)
{
char *rp;
rp = (char*)malloc(sizeof(Site)+n);
total_allocated += n;
((Site*)rp)->s.n = n;
((Site*)rp)->s.file = file;
((Site*)rp)->s.line = line;
return (void*)(rp + sizeof(Site));
}
void
debug_free(void *p, char *file, int line)
{
char *rp;
rp = ((char*)p) - sizeof(Site);
total_allocated -= ((Site*)rp)->s.n;
free(rp);
}
その場合、JNIライブラリは定期的(またはシャットダウン時)にtotal_allocated
変数の値をチェックして、それが妥当であることを確認する必要があります。前述のコードを拡張して、残された割当てをリンク・リストに保存し、リークしたメモリーがどこで割り当てられたかを報告することもできます。これは、単一セットのソース内のメモリー割り当てを追跡するための、局所的で移植可能な方法です。debug_free()がdebug_malloc()に由来するポインタのみを指定して呼び出されたことを確認する必要があり、realloc()、calloc()、strdup()などが使用されていた場合は、これらに対しても同じような関数を作成する必要があります。
より大域的な方法でネイティブ・ヒープのメモリー・リークを検出するには、プロセス全体のライブラリ呼び出しへの介入が必要になります。
ファイナライズを保留中のオブジェクトのモニター
ファイナライズが保留されているオブジェクトをモニターするための様々なコマンドやオプションが用意されています。
java.lang.OutOfMemoryError
エラーが「Java heap space」という詳細メッセージとともにスローされるのは、ファイナライザの多用が原因の可能性があります。これを診断するために、ファイナライズを保留しているオブジェクトの数をモニターするためのオプションが複数用意されています。
- JConsole管理ツールを使用して、ファイナライズを保留中のオブジェクト数をモニターできます。このツールでは、「サマリー」タブ・ペインのメモリー統計にファイナライズ保留中の数が報告されます。この数は概算ですが、アプリケーションを特徴付け、アプリケーションがファイナライズに大きく依存しているかどうかを理解するのに役立ちます。
- Linuxでは、
jmap
ユーティリティを-finalizerinfo
オプションとともに使用することで、ファイナライズを待機しているオブジェクトに関する情報を出力できます。 - アプリケーションで、
java.lang.management.MemoryMXBean
クラスのgetObjectPendingFinalizationCount
メソッドを使用して、ファイナライズを保留しているオブジェクトのおよその数を報告できます。APIドキュメントへのリンクおよびコード例は、「カスタム診断ツール」に記載されています。コード例を拡張して、ファイナライズ保留中の数の報告を簡単に追加できます。
java.lang.OutOfMemoryErrorエラーではないクラッシュのトラブルシューティング
致命的エラー・ログやクラッシュ・ダンプの情報を使用して、クラッシュをトラブルシューティングします。
ネイティブ・ヒープからの割当てが失敗した直後に、アプリケーションがクラッシュすることがあります。これは、メモリー割当て関数によって返されたエラーをチェックしないネイティブ・コードで発生します。
たとえば、malloc
システム・コールは使用可能なメモリーがない場合にnull
を返します。malloc
からの戻り値がチェックされない場合、アプリケーションは無効なメモリー位置にアクセスしようとしてクラッシュする可能性があります。状況によっては、このタイプの問題を特定することが難しいことがあります。
しかし、致命的エラー・ログやクラッシュ・ダンプの情報があれば、この問題を十分に診断できる場合もあります。致命的エラー・ログについては、「致命的エラー・ログ」で詳しく説明されています。割当ての失敗によってクラッシュが生じた場合は、割当ての失敗の理由を調べてください。ネイティブ・ヒープに関する他の問題と同様に、システムに十分な容量のスワップ空間が構成されていなかったり、システム上の別のプロセスがすべてのメモリー・リソースを消費していたり、システムのメモリー不足を引き起こすリークがアプリケーション内(またはそこから呼び出されたAPI内)で発生していたりする可能性があります。