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つは、アプリケーションが優先度の高いスレッドを作成しているため、ファイナライザ・スレッドがファイナライズ・キューを処理する速度よりキューの増加速度の方が速くなっている場合です。

処置: Javaヒープ・サイズを増やしてみてください。ファイナライズが保留されているオブジェクトのモニター方法については、「ファイナライズを保留中のオブジェクトのモニター」を参照してください。ファイナライズの検出とファイナライズからの移行の詳細は、『Java Platform, Standard Edition HotSpot仮想マシン・ガベージ・コレクション・チューニング・ガイド』「ファイナライズと弱参照、ソフト参照およびファントム参照」を参照してください。

詳細メッセージ: GCオーバーヘッド制限を超えました

原因: 詳細メッセージ「GCオーバーヘッド制限を超えました」は、ガベージ・コレクタ(GC)がほとんどの時間、実行されているため、Javaアプリケーションの処理がほとんど進んでいないことを示しています。ガベージ・コレクション後、Javaアプリケーションでガベージ・コレクションの実行に約98%を超える時間が費やされ、ヒープのリカバリが2%未満であり、その状態が直近5回(コンパイル時間定数)の連続するガベージ・コレクションで続いている場合に、java.lang.OutOfMemoryErrorエラーがスローされます。このエラーは通常、Javaヒープに新しい割当て用の空き領域がわずかしか残っておらず、ライブ・データの容量をほとんど格納できないためにスローされます。

処置: ヒープ・サイズを増やしてください。「GCオーバーヘッド制限を超えました」java.lang.OutOfMemoryErrorエラーは、コマンド行フラグ-XX:-UseGCOverheadLimitを使用すると無効にできます。

詳細メッセージ: 要求された配列サイズがVM制限を超えています

原因: リクエストされた配列サイズはVMの制限を超えていますという詳細メッセージは、使用可能なヒープ・サイズに関係なく、アプリケーション(またはアプリケーションが使用するAPI)がVM実装の制限より大きいサイズの配列を割り当てようとしたことを示します。

処置: アプリケーション(またはアプリケーションが使用するAPI)が、VM実装の制限より小さいサイズの配列を割り当てることを確認してください

詳細メッセージ: メタスペース

原因: Javaクラス・メタデータ(Javaクラスの仮想マシン内部表現)がネイティブ・メモリー(ここでは、メタスペースと呼ばれる)に割り当てられています。クラス・メタデータのメタスペースが枯渇すると、詳細メッセージ「メタスペース」とともにjava.lang.OutOfMemoryErrorエラーがスローされます。クラス・メタデータに使用できるメタスペース容量は、コマンド行に指定するパラメータMaxMetaSpaceSizeにより制限されます。クラス・メタデータに必要なネイティブ・メモリー容量がMaxMetaSpaceSizeを超過すると、詳細メッセージ「メタスペース」とともにjava.lang.OutOfMemoryErrorエラーがスローされます。

処置: コマンド行でMaxMetaSpaceSizeが指定されている場合は、その値を増やしてください。メタスペースは、Javaヒープと同じアドレス空間から割り当てられます。Javaヒープのサイズを減らすと、メタスペースに使用できる領域が増えます。このトレードオフが役立つのは、Javaヒープに余分な空き領域がある場合のみです。次に示すスワップ領域の不足に関する詳細メッセージの「処置」を参照してください。

詳細メッセージ: reasonのためにsizeバイトを要求します。スワップ領域が不足していますか。

原因: 詳細メッセージreasonのためにsizeバイトを要求します。スワップ領域が不足していますか。」は、java.lang.OutOfMemoryErrorエラーであるように見えます。しかし、ネイティブ・ヒープからの割当てが失敗し、ネイティブ・ヒープが枯渇寸前になっている可能性があるときに、Javaはこの見かけ上のエラーを報告します。このメッセージは、失敗した要求のサイズ(バイト数)とメモリー要求の理由を示しています。通常、reasonは割当ての失敗を報告するソース・モジュールの名前になりますが、場合によっては実際の理由を示すこともあります。

処置: このエラーがスローされると、Java VM (JVM)は致命的エラー処理メカニズムを呼び出します。クラッシュ発生時のスレッド、プロセス、およびシステムに関する有用な情報を含む、致命的エラー・ログ・ファイルを生成します。ネイティブ・ヒープ不足の場合は、ログ内のヒープ・メモリーとメモリー・マップの情報が役立つ可能性があります。「致命的エラー・ログ」を参照してください。

オペレーティング・システムのトラブルシューティング・ユーティリティを使用して、問題をより詳しく診断する必要がある場合があります。「オペレーティング・システムのネイティブ・ツール」を参照してください。

詳細メッセージ: 圧縮クラス領域

原因: 64ビット・プラットフォーム上で、クラス・メタデータへのポインタが32ビットのオフセットで表現されている可能性があります(UseCompressedOopsを使用)。これはコマンド行フラグUseCompressedClassPointersで制御されます(デフォルトではtrue)。UseCompressedClassPointerstrueの場合、クラス・メタデータに使用できる領域量はCompressedClassSpaceSizeの容量で固定されます。UseCompressedClassPointersに必要な領域がCompressedClassSpaceSizeを超える場合、詳細メッセージ「圧縮クラス領域」とともにjava.lang.OutOfMemoryErrorエラーがスローされます。

処置: CompressedClassSpaceSizeを増やすか、UseCompressedClassPointersfalseに設定します。

ノート:

CompressedClassSpaceSizeの許容サイズには制限があります。たとえば、-XX:CompressedClassSpaceSize=4gは、許容制限を超えているため、次のようなメッセージが表示されます
CompressedClassSpaceSize of 4294967296 is invalid; must be between 1048576 and 3221225472.

ノート:

クラス・メタデータには複数の種類(–klassメタデータやその他のメタデータ)があります。CompressedClassSpaceSizeによって制限された領域に格納されるのは、klassメタデータのみです。その他のメタデータはメタスペースに格納されます。
詳細メッセージ: reason stack_trace (ネイティブ・メソッド)

原因: この詳細メッセージは、ネイティブ・メソッドで割当て失敗が発生したことを示しています。これと前のメッセージとの違いは、割当ての失敗がJVM自体ではなく、Java Native Interface (JNI)またはネイティブ・メソッドで検出されたことです。

処置: このタイプのjava.lang.OutOfMemoryErrorエラーがスローされた場合は、オペレーティング・システムのネイティブ・ユーティリティを使用して、問題をより詳しく診断する必要がある場合があります。「オペレーティング・システムのネイティブ・ツール」を参照してください。

メモリー・リークの検出

java.lang.OutOfMemoryErrorエラーは、Javaアプリケーションのメモリー・リークの兆候である可能性があります。また、Javaヒープ、メタスペースまたは圧縮クラス領域のいずれかが、その特定のメモリー・プールに対するアプリケーションのメモリー要件よりも小さいサイズであることを示している可能性もあります。アプリケーションでメモリー・リークが発生していると想定する前に、java.lang.OutOfMemoryErrorエラーが発生しているメモリー・プールのサイズが適切であることを確認します。Javaヒープのサイズは、–Xmxおよび–Xmsコマンド行オプションを使用して設定でき、メタスペースの最大サイズおよび初期サイズは、MaxMetaspaceSizeおよびMetaspaceSizeを使用して構成できます。同様に、圧縮クラス領域のサイズは、CompressedClassSpaceSizeオプションを使用して設定できます。

メモリー・リークは、特に低速のものは検出が非常に困難な場合が多くあります。メモリー・リークは、Javaオブジェクトまたクラスへの参照がアプリケーションにより誤って保持され、ガベージ・コレクションによって解放されない場合に発生します。誤って保持されているこれらのオブジェクトまたはクラスは、時間の経過とともにメモリー内で増大し、最終的にJavaヒープまたはメタスペース全体をいっぱいにしてしまい、頻繁にガベージ・コレクションが発生して、最終的にはプロセスがjava.lang.OutOfMemoryErrorエラーで終了します。

メモリー・リークを検出するには、アプリケーションのライブ・セット(つまり、完全なガベージ・コレクション後に使用されるJavaヒープ領域またはメタスペースの量)をモニターすることが重要です。アプリケーションが安定した状態になり、安定した負荷がかかった後、ライブ・セットが時間の経過とともに増加する場合は、メモリー・リークの強い兆候である可能性があります。アプリケーションのライブ・セットおよびメモリー使用量は、JConsoleおよびJDK Mission Controlを使用してモニターできます。メモリー使用量の情報は、ガベージ・コレクション・ログから抽出することもできます。

エラーの詳細メッセージがネイティブ・ヒープの枯渇を示している場合は、アプリケーションでネイティブ・メモリー・リークが発生している可能性があります。ネイティブ・メモリー・リークを確認するには、pmapPerfMonなどのネイティブ・ツールを使用し、定期的に収集される出力を比較して、プロセスの新しく割り当てられた、または増大しているメモリー・セクションを判断します。

JConsole

JConsoleは、Javaアプリケーションのリソースをモニターするための優れたツールです。特に、Javaヒープ、メタスペース、圧縮クラス領域、CodeHeapの世代など、アプリケーションの様々なメモリー・プールの使用量をモニターするのに役立ちます。

次のスクリーンショットでは、プログラムの例として、JConsoleはヒープ・メモリーおよび古い世代の使用量が一定期間にわたって着実に増加していることを示しています。完全ガベージ・コレクションを数回行った後でも、このようにメモリー使用量が着実に増加している場合は、メモリー・リークが発生していることを示しています。

図3-1 JConsoleのヒープ・メモリー

JConsoleのヒープ・メモリー

図3-2 JConsoleの古い世代

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)が開きます。「自動分析結果」ページを参照してください。メモリー・リークを検出するには、ページの「ライブ・オブジェクト」セクションにフォーカスを置きます。次に、ヒープ・サイズの問題を示す記録の例を示します:

図3-3 メモリー・リーク - 自動分析ページ

図3-3の説明が続きます
「図3-3 メモリー・リーク - 自動分析ページ」の説明

「ヒープ・ライブ・セットの傾向」セクションで、ヒープのライブ・セットが急激に増加し、参照ツリーの分析でリーク候補が検出されたことがわかります。

さらに分析するには、「Javaアプリケーション」ページを開き、「メモリー」ページをクリックします。次に、メモリー・リークの問題を示す記録のサンプル図を示します。

図3-4 メモリー・リーク-「メモリー」ページ

図3-4の説明が続きます
「図3-4メモリー・リーク-「メモリー」ページ」の説明

グラフから、メモリー使用量が徐々に増加し、メモリー・リークの問題が示されていることを確認できます。

ガベージ・コレクションのログ

メモリー使用量の情報は、GCログを使用して抽出することもできます。アプリケーションが古い世代またはメタスペースで領域を再利用しようとして完全ガベージ・コレクションを何度か実行したことがGCログに示されているが、大きなメリットがなかった場合は、アプリケーションでメモリー・リークの問題が発生している可能性があります。

GCログは、–Xlogコマンド行Javaオプションを使用して収集できます。次に例を示します。
-Xlog:gc*,gc+phases=debug:gc.log

これにより、少なくともgcでタグ付けされたメッセージはinfoレベルで、正確にgcphasesでタグ付けされたメッセージは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ソース・コード内のメモリー・リークを診断するために使用できるツールについて説明します。

診断データ

この項では、メモリー・リークのトラブルシューティングに使用できる診断データについて説明します。

ヒープ・ヒストグラム

ヒープ・ヒストグラムを調べることで、メモリー・リークをすばやく絞り込んでみることができます。ヒープ・ヒストグラムはいくつかの方法で取得できます。

  • -XX:+PrintClassHistogramコマンド行オプションを指定してJavaプロセスを起動すると、Ctrl+Breakハンドラによってヒープ・ヒストグラムが生成されます。
  • jmapユーティリティを使用して、実行中のプロセスからヒープ・ヒストグラムを取得できます。

    診断機能を強化し、パフォーマンスのオーバーヘッドを削減するには、jmap,ユーティリティのかわりに最新のユーティリティjcmdの使用をお薦めします。「jcmdユーティリティの有用なコマンド」を参照してください。次の例のコマンドでは、jcmdを使用して実行中のプロセスのヒープ・ヒストグラムを作成しますが、次のjmapコマンドと同様な結果が得られます。

    jcmd <process id/main class> GC.class_histogram filename=Myheaphistogram
    
    jmap -histo pid
    

    この出力には、ヒープ内の各クラス型の合計サイズとインスタンス数が表示されます。ヒストグラムを連続して(たとえば、2分ごとに)取得すると、詳細な分析につなげることができる傾向が見える場合があります。

  • 次の例に示すように、jhsdb jmapユーティリティを使用してコア・ファイルからヒープ・ヒストグラムを取得できます。
    jhsdb jmap --histo --exe jdk-home/bin/java --core core_file
    

    たとえば、アプリケーションの実行中に-XX:+CrashOnOutOfMemoryErrorコマンド行オプションを指定すると、java.lang.OutOfMemoryErrorエラーがスローされたときに、JVMによってコア・ダンプが生成されます。その後、次の例に示すように、コア・ファイルに対してjhsdb jmapを実行すると、ヒストグラムを取得できます。

    $ jhsdb jmap --histo --exe /usr/java/jdk-11/bin/java --core core.21844
    Attaching to core core.21844 from executable /usr/java/jdk-11/bin/java, please wait...
    Debugger attached successfully.
    Server compiler detected.
    JVM version is 11-ea+24
    Iterating over heap. This may take a while...
    Object Histogram:
    
    num     #instances     #bytes   Class description
    --------------------------------------------------------------------------
    1:            2108     112576    byte[]
    2:             546      66112    java.lang.Class
    3:            1771      56672    java.util.HashMap$Node
    4:             574      53288    java.lang.Object[]
    5:            1860      44640    java.lang.String
    6:             349      40016    java.util.HashMap$Node[]
    7:              16      33920    char[]
    8:             977      31264    java.util.concurrent.ConcurrentHashMap$Node
    9:             327      15696    java.util.HashMap
    10:            266      13800    java.lang.String[]
    11:            485      12880    int[]
    :
    
    Total : 14253 633584
    Heap traversal took 1.15 seconds.

    前述の例は、byte配列の数(ヒープ内の2108個のインスタンス)によってjava.lang.OutOfMemoryErrorエラーが生じたことを示しています。バイト配列がどこで割り当てられたかは、詳細な分析を行わないとはっきりしません。それでもなおこの情報は有用です。

ヒープ・ダンプ

ヒープ・ダンプは、メモリー・リークのトラブルシューティングに最も重要なデータです。ヒープ・ダンプは、jcmdjmap、JConsoleツールおよび-XX:+HeapDumpOnOutOfMemoryError Javaオプションを使用して収集できます

  • 次に示すように、GC.heap_dumpコマンドをjcmdユーティリティとともに使用して、ヒープ・ダンプを作成できます:
    jcmd <process id/main class> GC.heap_dump filename=heapdump.dmp
  • -dump:format=bを含むjmapは、実行中のプロセスからヒープをダンプできます
    jmap -dump:format=b,file=snapshot.jmap <process id>
  • JConsoleのMBeanブラウザでは、HotSpotDiagnostic MBeanを使用できるようになり、これを使用して、アタッチされたJavaプロセスのヒープ・ダンプを作成できます。
  • -XX:+HeapDumpOnOutOfMemoryError Javaオプションを使用すると、java.lang.OutOfMemoryErrorエラーで障害が発生した場合に、プロセスのJavaヒープをダンプできます。
Javaフライト記録

ヒープ統計を有効にして収集されたフライト記録は、メモリー・リークのトラブルシューティングに役立ちます。これにより、Javaオブジェクトと、Javaヒープの上位の増加が時系列で表示されます。ヒープ統計を有効にするには、JDK Mission Control (JMC)を使用し、次に示すように、「ウィンドウ」→「フライト記録テンプレート・マネージャ」に移動して、ヒープ統計を有効にします。

図3-5 フライト記録テンプレート・マネージャ

フライト記録テンプレート・マネージャ
ヒープ統計は、.jfcファイルを手動で編集し、heap-statistics-enabledtrueに設定して有効にすることもできます。
<event path="vm/gc/detailed/object_count">
    <setting name="enabled" control="heap-statistics-enabled">true</setting>
    <setting name="period">everyChunk</setting>
</event>

その後、次のいずれかの方法でフライト記録を作成できます:

  • Javaフライト・レコーダのオプション
    -XX:StartFlightRecording=delay=20s,duration=60s,name=MyRecording,filename=myrecording.jfr,settings=profile
  • JMC
クラス・ローダーの統計

クラス・ローダーおよびそれによってロードされるクラスの数に関する情報は、メタスペースおよび圧縮クラス領域に関連するメモリー・リークを診断する際に非常に役立ちます。

クラス・ローダーの統計情報は、次の例に示すように、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フライト記録を使用して、リーク・オブジェクトを識別できます。リーク・クラスを検索するには、「メモリー」ページを開き、「ライブ・オブジェクト」ページをクリックします。次に、リーク・クラスを示す記録のサンプル図を示します。

図3-6 メモリー・リーク - 「ライブ・オブジェクト数」ページ

図3-6の説明が続く
「図3-6 メモリー・リーク - 「ライブ・オブジェクト数」ページ」の説明

追跡されているライブ・オブジェクトのほとんどがLeak$DemoThreadによって保持されており、このオブジェクトはリークされたchar[]クラスを保持していることがわかります。詳細な分析は、存続したオブジェクトのサンプリングを含む「結果」タブの古いオブジェクト・サンプル・イベントを参照してください。このイベントには、割当て時間、割当てスタック・トレース、およびGCルートに戻るパスが含まれます。

リークの可能性があるクラスが特定されたら、「JVM内部」ページの「TLAB割当て」ページで、オブジェクトが割り当てられた場所のサンプルを調べます。次に、TLAB割当てを示す記録のサンプルを示します。

図3-7 メモリー・リーク - TLAB割当て

図3-7の説明が続く
「図3-7 メモリー・リーク - 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で使用されるネイティブ・メモリーは、ツール自体によって追跡されます。
次の例では、JConsoleのスクリーンショットは、圧縮クラス領域の使用量が時間の経過とともに一定の割合で増加していることを示しています。

図3-8 JConsole圧縮クラス領域

JConsole - 圧縮クラス領域
この使用量の増加を診断するために、NMTを使用してJavaプロセスを追跡できます。ベースラインと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システムには、割り当て追跡を扱う際に役立つ、mtracelibnjamdなどのツールがあります。

次の項では、これらのツールのうち2つについて詳しく説明します。

Valgrind

Valgrindは、Linuxでのネイティブ・メモリー・リークの診断に使用できます。Valgrindを使用してJavaプロセスをモニターするには、次のように起動します:
$ 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
   ...
}
前述のコマンドを使用して抑制を配置すると、Valgrindは特定されたリークを指定されたログ・ファイルに書き込みます。次に例を示します。
==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 Platform, Standard Edition HotSpot仮想マシン・ガベージ・コレクション・チューニング・ガイド』「ファイナライズと弱参照、ソフト参照およびファントム参照」を参照してください。

java.lang.OutOfMemoryErrorエラーではないクラッシュのトラブルシューティング

致命的エラー・ログやクラッシュ・ダンプの情報を使用して、クラッシュをトラブルシューティングします。

ネイティブ・ヒープからの割当てが失敗した直後に、アプリケーションがクラッシュすることがあります。これは、メモリー割当て関数によって返されたエラーをチェックしないネイティブ・コードで発生します。

たとえば、mallocシステム・コールは使用可能なメモリーがない場合にnullを返します。mallocからの戻り値がチェックされない場合、アプリケーションは無効なメモリー位置にアクセスしようとしてクラッシュする可能性があります。状況によっては、このタイプの問題を特定することが難しいことがあります。

しかし、致命的エラー・ログやクラッシュ・ダンプの情報があれば、この問題を十分に診断できる場合もあります。致命的エラー・ログについては、「致命的エラー・ログ」で詳しく説明されています。割当ての失敗によってクラッシュが生じた場合は、割当ての失敗の理由を調べてください。ネイティブ・ヒープに関する他の問題と同様に、システムに十分な容量のスワップ空間が構成されていなかったり、システム上の別のプロセスがすべてのメモリー・リソースを消費していたり、システムのメモリー不足を引き起こすリークがアプリケーション内(またはそこから呼び出されたAPI内)で発生していたりする可能性があります。