3 メモリー・リークのトラブルシューティング

この章では、メモリー・リークが関与する問題の診断について、いくつかのアドバイスを行います。

アプリケーションの実行時間がしだいに長くなる場合や、オペレーティング・システムの実行速度がしだいに低下しているように思われる場合、これはメモリー・リークの兆候である可能性があります。つまり、仮想メモリーが割り当てられているけれども、それが不要になったときに返されません。最終的には、アプリケーションまたはシステムがメモリーを使い果たし、アプリケーションが異常終了します。

この章の構成は、次のとおりです。

Javaフライト・レコーダによるメモリー・リークのデバッグ

Javaフライト・レコーダは、JavaランタイムおよびJavaランタイムで実行されているJavaアプリケーションの詳細情報を記録します。

次の項では、図を示して、Javaフライト・レコーダでメモリー・リークをデバッグする方法を説明します。

メモリー・リークの検出

Javaフライト記録を使用してメモリー・リークを早期に検出し、OutOfmemoryErrorsを防止します。

スロー・メモリー・リークの検出は難しい場合があります。一般的な症状は、頻繁なガベージ・コレクションにより実行に時間がかかり、その後、アプリケーションが低速になります。やがてOutOfmemoryErrorsが表示される可能性があります。ただし、Javaフライト・レコーダを使用すると、問題が発生する前であっても、メモリー・リークを早期に検出できます。

アプリケーションのライブ・セットの経時的な増加がないか確認します。ライブ・セットは、Oldコレクションの後(すべての非ライブ・オブジェクトがガベージ・コレクトされた後)に使用されるJavaヒープ容量です。ライブ・セットは、様々な方法で調査できます。たとえば、-verbosegcオプションを使用して実行するか、JMC JMXコンソールを使用してJVMに接続し、com.sun.management.GarbageCollectorAggregator MBeanを見ます。しかし、別の簡単なアプローチは、フライト記録を取ることです。

記録を開始するときにヒープ統計を有効にすると、記録の開始時と終了時にOldコレクションがトリガーされます。これにより、アプリケーションに若干の遅延が生じる場合があります。しかし、ヒープ統計は、正確なライブ・セット情報を生成します。やや急速なメモリー・リークが疑われる場合は、たとえば、1時間以上実行されるプロファイリング記録を取ります。図3-1に示すように、「メモリー」タブをクリックし、「ガベージ・コレクション」タブを選択して、最初と最後のOldコレクションを確認します。

図3-1 メモリー・リークのデバッグ - 「ガベージ・コレクション」タブ

図3-1の説明が続きます
「図3-1 メモリー・リークのデバッグ - 「ガベージ・コレクション」タブ」の説明

図3-1,に示すように、最初のOldコレクションを選択して、GC後のヒープ・データとヒープ使用量を調べます。この記録では、34.10MBです。今度は、最後のOldコレクションからの同じデータを調べて、ライブ・セットが増加したかどうかを確認します。記録を取る前に、アプリケーションが起動し、安定した状態に到達できるようにする必要があります。

ゆっくりしたリークの場合は、5分間の短時間の記録を取ることができます。次に、たとえば、(メモリー・リークの速さに応じて)24時間後に、もう1回記録を取ります。明らかに、ライブ・セットは上下しますが、時間の経過とともに着実に増加している場合は、メモリー・リークの可能性があります。

リーク・クラスの調査

Javaフライト記録を使用して、メモリー・リークを特定します。

記録がリークを示したら、「オブジェクト統計」を調べます。1つの長い記録を検討し、記録を通してヒープ使用量が最も増加したクラスを調べます。間隔を置いて複数の記録を取った場合は、図3-2に示すように、「ヒープの内容」セクションを比較し、どのオブジェクト・タイプが記録間で最も増加しているかを調べます。

図3-2 メモリー・リークのデバッグ - リーク・クラスの調査

図3-2の説明が続きます
「図3-2 メモリー・リークのデバッグ - リーク・クラスの調査」の説明

特に、標準ライブラリの一部ではないクラスに注目してください。たとえば、多くの場合、最大の増加率の1つとしてChar配列が表示されます。これは、多くのStringsが割り当てられているためです。したがって、これらのStringsを維持しているオブジェクトに注意します。10個のStringsをメンバーとして持つクラスがある場合、オブジェクト自体がヒープを使いすぎることはありません。ヒープは、主にChar配列へのポインタが含まれているStringsによって使用されます。したがって、オブジェクトのサイズではなく、インスタンスの数でソートするのが適切です。いずれかのアプリケーション・クラスに多くのインスタンスがある場合、それらのオブジェクトが他のオブジェクトを維持している可能性があります。

リークの調査

Javaフライト記録を使用した追加情報を使用してメモリー・リークを特定するためのヒント。

path-to-gc-rootsパラメータでは、記録の終了時にガベージ・コレクション(GC)ルートへのパスを収集するかどうかを指定します。パス情報はメモリー・リークを探すのに役立ちますが、この収集には時間がかかります。メモリー・リークが疑われるアプリケーションの記録を開始する場合にのみ、このフラグをオンにします。デフォルトでは、このパラメータは無効になっています。

アプリケーションの起動時にこのフラグをオンにするには、javaコマンドの-XX:StartFlightRecording=parameter=valueオプションを使用します。
-XX:StartFlightRecording=path-to-gc-roots=true

すでに実行中のアプリケーションでこのフラグをオンにするには、jcmdユーティリティのJFR.startまたはJFR.dumpコマンドでpath-to-gc-rootsパラメータを使用します。

jcmd pid JFR.start path-to-gc-roots=true

Javaフライト記録を使用して、いくつかの追加情報を得ることができます。

図3-3に示すように、「割当て」サブタブで、オブジェクトの割当先のサンプルを見ます。

図3-3 メモリー・リークのデバッグ - 「割当て」タブ

図3-3の説明が続きます
「図3-3 メモリー・リークのデバッグ - 「割当て」タブ」の説明

特定のクラスのリークが予想される場合は、「新しいTLABの割当て」タブを見てください。割り当てられているクラス・サンプルをチェックします。スロー・リークの場合は、このオブジェクトの割当てが少なく、サンプルがないこともあります。また、特定の割当てサイトのみがリークの原因となっている可能性もあります。要約すると、これによってリークの適切な割当てスタック・トレースに到達することは保証されませんが、重要な手がかりを得る可能性があります。

OutOfMemoryError例外の理解

java.lang.OutOfMemoryErrorのエラーは、Javaヒープにオブジェクトを割り当てるための十分な空間がないときにスローされます。

メモリー・リークの一般的な兆候の1つは、java.lang.OutOfMemoryError例外です。この場合、ガベージ・コレクタによって新しいオブジェクトを格納するための空間を確保することも、ヒープをこれ以上拡張することもできません。また、このエラーは、Javaクラスのロードをサポートするための十分なネイティブ・メモリーがないときにスローされる場合もあります。ガベージ・コレクションの実行に過剰な時間が消費され、メモリーがほとんど解放されていない場合、まれにjava.lang.OutOfMemoryErrorがスローされることがあります。

java.lang.OutOfMemoryError例外がスローされるときに、スタック・トレースも出力されます。

java.lang.OutOfMemoryError例外は、ネイティブ割当てを満たすことができないとき(たとえば、スワップ空間が少ない場合)に、ネイティブ・ライブラリ・コードによってスローされることもあります。

OutOfMemoryError例外を診断する最初の手順は、その例外の原因を突き止めることです。スローされた理由が、Javaヒープがいっぱいであるからなのか、それともネイティブ・ヒープがいっぱいであるからなのかを調べます。原因を見つけることができるように、例外テキストの最後に詳細なメッセージが含まれています(次の例外を参照)。

Exception in thread thread_name: java.lang.OutOfMemoryError: Java heap space

原因: Java heap spaceという詳細メッセージは、Javaヒープ内でオブジェクトを割り当てることができなかったことを示しています。このエラーは、必ずしもメモリー・リークを意味しません。この問題は、指定されたヒープ・サイズ(指定されていない場合はデフォルト・サイズ)がアプリケーションにとって十分でないという、単純な構成の問題である可能性があります。

それ以外の場合、特に長期にわたって稼動するアプリケーションでは、このメッセージはアプリケーションが誤ってオブジェクトへの参照を保持しているため、オブジェクトのガベージ・コレクションができないことを示している可能性があります。これは、Java言語ではメモリー・リークに相当します。注意: アプリケーションによって呼び出されたAPIが誤ってオブジェクト参照を保持している可能性もあります。

このエラーのもう1つの潜在的な原因は、ファイナライザを過度に使用するアプリケーションで発生します。クラスにfinalizeメソッドがある場合、そのタイプのオブジェクトの空間はガベージ・コレクション時に再利用されません。かわりに、ガベージ・コレクション後にファイナライズ用のキューにオブジェクトが入れられ、後でファイナライズが実行されます。Oracle Sunの実装では、ファイナライザはファイナライズ・キューを提供するデーモン・スレッドによって実行されます。ファイナライザ・スレッドがファイナライズ・キューを処理しきれなかった場合は、Javaヒープがいっぱいになる可能性があり、このタイプのOutOfMemoryError例外がスローされます。この状況を発生させるシナリオの1つは、アプリケーションが優先度の高いスレッドを作成しているため、ファイナライザ・スレッドがファイナライズ・キューを処理する速度よりキューの増加速度の方が速くなっている場合です。

処置: ファイナライズが保留されているオブジェクトのモニター方法については、「ファイナライズを保留中のオブジェクトのモニター」を参照してください。

Exception in thread thread_name: java.lang.OutOfMemoryError: GC Overhead limit exceeded

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

処置: ヒープ・サイズを増やしてください。GC Overhead limit exceededjava.lang.OutOfMemoryError例外は、コマンド行フラグ-XX:-UseGCOverheadLimitを使用すると無効にできます。

Exception in thread thread_name: java.lang.OutOfMemoryError: Requested array size exceeds VM limit

原因: 「Requested array size exceeds VM limit」という詳細メッセージは、アプリケーション(またはそのアプリケーションが使用するAPI)がヒープ・サイズより大きい配列を割り当てようとしたことを示しています。たとえば、アプリケーションが512MBの配列を割り当てようとしたが、最大ヒープ・サイズが256MBだった場合は、「Requested array size exceeds VM limit」という理由とともにOutOfMemoryErrorがスローされます。

処置: 通常、この問題は構成の問題(ヒープ・サイズが小さすぎる)であるか、アプリケーションが非常に大きい配列を作成しようとする結果をもたらすバグ(たとえば、不正なサイズを計算するアルゴリズムを使用して配列内の要素の数が計算された場合など)です。

Exception in thread thread_name: java.lang.OutOfMemoryError: Metaspace

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

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

Exception in thread thread_name: java.lang.OutOfMemoryError: request size bytes for reason.Out of swap space?

原因: 「request size bytes for reason.Out of swap space?」という詳細メッセージは、OutOfMemoryError例外であるように見えます。しかし、ネイティブ・ヒープからの割当てが失敗し、ネイティブ・ヒープが枯渇寸前になっている可能性があるときに、Java HotSpot VMコードはこの見かけ上の例外を報告します。このメッセージは、失敗した要求のサイズ(バイト数)とメモリー要求の理由を示しています。通常、その理由は割当ての失敗を報告するソース・モジュールの名前になりますが、場合によっては実際の理由になることもあります。

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

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

Exception in thread thread_name: java.lang.OutOfMemoryError: Compressed class space

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

処置: CompressedClassSpaceSizeを増やして、UseCompressedClassPointersをオフにしてください。注意: CompressedClassSpaceSizeの許容サイズには制限があります。たとえば、-XX: CompressedClassSpaceSize=4gが許容制限を超えると、次のようなメッセージが表示されます。

CompressedClassSpaceSize of 4294967296 is invalid; must be between 1048576 and 3221225472. (CompressedClassSpaceSizeの4294967296は無効です。1048576から3221225472の間に設定する必要があります。)

注意:

クラス・メタデータには複数の種類(-klassメタデータやその他のメタデータ)があります。CompressedClassSpaceSizeによって制限された領域に格納されるのは、klassメタデータのみです。その他のメタデータはMetaspaceに格納されます。
Exception in thread thread_name: java.lang.OutOfMemoryError: reason stack_trace_with_native_method

原因: エラー・メッセージの詳細部分が「reason stack_trace_with_native_method」であり、最上位フレームがネイティブ・メソッドであるスタック・トレースが出力される場合、これはネイティブ・メソッドで割当て失敗が発生したことを示しています。これと前のメッセージとの違いは、割当ての失敗がJVMコードではなく、Java Native Interface (JNI)またはネイティブ・メソッドで検出されたことです。

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

OutOfMemoryErrorのかわりにクラッシュのトラブルシューティング

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

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

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

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

Java言語コードでのリークの診断

NetBeansプロファイラを使用してJava言語コードのリークを診断します。

Java言語コード内のリークの診断は難しい可能性があります。通常、アプリケーションの非常に詳しい知識が必要です。さらに、プロセスが反復し、冗長なことがよくあります。この項では、Java言語コード内のメモリー・リークを診断するために使用できるツールについて説明します。

注意:

この項で説明したツールに加えて、数多くのサードパーティ・メモリー・デバッガ・ツールが利用可能です。メモリー・デバッグ機能を備えた商用ツールの例として、Eclipse Memory Analyzer Tool (MAT)とYourKit (www.yourkit.com)の2つがあります。他にも数多くありますが、推奨される特定の製品はありません。

Java言語コードのリークの診断に使用されるユーティリティを次に示します。

  1. NetBeansプロファイラ: NetBeansプロファイラは、メモリー・リークの場所をすばやく見つけることができます。商用のメモリー・リーク・デバッグ・ツールは、大規模なアプリケーション内のリークを特定するのに長い時間がかかる場合があります。しかし、NetBeans Profilerは、このようなオブジェクトが一般的に示すメモリーの割り当てと再利用のパターンを使用します。このプロセスには、メモリー再利用の欠落も含まれます。このプロファイラは、これらのオブジェクトがどこで割り当てられたかをチェックできます(リークの根本原因を特定するには、通常はこれで十分です)。

    NetBeansプロファイラに関する項を参照してください。

次の項では、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コマンド行オプションを指定すると、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個のインスタンス)によってOutOfMemoryError例外が生じたことを示しています。バイト配列がどこで割り当てられたかは、詳細な分析を行わないとはっきりしません。それでもなおこの情報は有用です。

ファイナライズを保留中のオブジェクトのモニター

ファイナライズが保留されているオブジェクトをモニターするには、様々なコマンドやオプションを使用できます。

OutOfMemoryError例外が「Java heap space」という詳細メッセージとともにスローされた場合、その原因はファイナライザの過度の使用によるものである可能性があります。これを診断するために、ファイナライズを保留しているオブジェクトの数をモニターするためのオプションが複数用意されています。

  • JConsole管理ツールを使用して、ファイナライズを保留中のオブジェクト数をモニターできます。このツールでは、「サマリー」タブ・ペインのメモリー統計にファイナライズ保留中の数が報告されます。この数は概算ですが、アプリケーションを特徴付け、アプリケーションがファイナライズに大きく依存しているかどうかを理解するために使用できます。
  • Oracle SolarisおよびLinuxオペレーティング・システムでは、jmapユーティリティを-finalizerinfoオプションとともに使用することで、ファイナライズを待機しているオブジェクトに関する情報を出力できます。
  • アプリケーションで、java.lang.management.MemoryMXBeanクラスのgetObjectPendingFinalizationCountメソッドを使用して、ファイナライズを保留しているオブジェクトのおよその数を報告できます。APIドキュメントへのリンクおよびコード例は、「カスタム診断ツール」に記載されています。コード例を拡張して、ファイナライズ保留中の数の報告を簡単に追加できます。

ネイティブ・コードでのリークの診断

複数の手法を使用してネイティブ・コードのメモリー・リークを検出し、切り分けることができます。一般に、すべてのプラットフォームに対応できる理想的な解決方法はありません。

ネイティブ・コードを診断するためのいくつかの手法を次に示します。

すべてのメモリー割当ておよび解放呼出しの追跡

ツールを使用して、すべてのメモリー割当ておよびそのメモリーの使用方法を追跡できます。

非常によく使われる方法は、ネイティブ割当てのすべての割当て呼び出しと解放呼出しを追跡することです。これは、かなり簡単なプロセスになる場合と、非常に高度なプロセスになる場合があります。ネイティブ・ヒープの割り当てとそのメモリーの使用の追跡をめぐっては、長年にわたって多くの製品が構築されています。

IBM Rational PurifyのようなツールやSun Studioのdbxデバッガの実行時チェック機能を使用すると、ネイティブ・コードの通常の状況でこれらのリークを検出したり、初期化されていないメモリーへの割当てや解放されたメモリーへのアクセスを表すネイティブ・ヒープ・メモリーへのアクセスを検出したりできます。「dbxデバッガによるリークの検出」を参照してください。

これらのタイプのツールのすべてがネイティブ・コードを使用するJavaアプリケーションで機能するわけではなく、これらのツールは通常はプラットフォーム固有です。仮想マシンはコードを実行時に動的に作成するため、これらのツールはコードを誤って解釈したり、まったく動作しなかったり、誤った情報を与えたりする可能性があります。ツールのベンダーに問い合せて、ツールのバージョンが、使用している仮想マシンのバージョンで動作することを確認してください。

多数の簡単で移植可能なネイティブ・メモリー・リークの検出例は、sourceforgeを参照してください。ほとんどのライブラリやツールは、アプリケーションのソースを再コンパイルまたは編集して、割当て関数にラッパー関数を被せることができることを前提としています。より強力なツールでは、これらの割当て関数に動的に介入することで、アプリケーションを変更せずに実行できます。これは、Oracle Solaris 9オペレーティング・システムupdate 3で最初に導入されたlibumem.soライブラリのケースです(「libumemツールによるリークの検出」を参照)。

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()などが使用されていた場合は、これらに対しても同じような関数を作成する必要があります。

より大域的な方法でネイティブ・ヒープのメモリー・リークを検出するには、プロセス全体のライブラリ呼び出しへの介入が必要になります。

オペレーティング・システム・サポートによるメモリー割当ての追跡

ツールを使用して、オペレーティング・システムのメモリー割当てを追跡できます。

ほとんどのオペレーティング・システムには、なんらかの形式の大域的割り当て追跡サポートが含まれています。

  • Windowsでは、MSDNライブラリでデバッグ・サポートを検索してください。Microsoft C++コンパイラには、メモリー割当てを追跡するための追加サポートを自動的に取り込む、/Mdおよび/Mddコンパイラ・オプションがあります。
  • Linuxシステムには、割り当て追跡を扱う際に役立つ、mtracelibnjamdなどのツールがあります。
  • Oracle Solarisオペレーティング・システムには、watchmallocツールが用意されています。Oracle Solaris 9オペレーティング・システムUpdate 3では、libumemツールも提供されています。「libumemツールによるリークの検出」を参照してください。

dbxデバッガによるリークの検出

dbxデバッガには、リークを検出できる実行時チェック(RTC)機能が含まれています。dbxデバッガはOracle Solaris Studioの一部で、Linuxでも使用できます。

次の例はdbxセッションのサンプルです。

$ dbx ${java_home}/bin/java
Reading java
Reading ld.so.1
Reading libthread.so.1
Reading libdl.so.1
Reading libc.so.1
(dbx) dbxenv rtc_inherit on
(dbx) check -leaks
leaks checking - ON
(dbx) run HelloWorld
Running: java HelloWorld 
(process id 15426)
Reading rtcapihook.so
Reading rtcaudit.so
Reading libmapmalloc.so.1
Reading libgen.so.1
Reading libm.so.2
Reading rtcboot.so
Reading librtc.so
RTC: Enabling Error Checking...
RTC: Running program...
dbx: process 15426 about to exec("/net/bonsai.sfbay/export/home2/user/ws/j2se/build/solaris-i586/bin/java")
dbx: program "/net/bonsai.sfbay/export/home2/user/ws/j2se/build/solaris-i586/bin/java"
just exec'ed
dbx: to go back to the original program use "debug $oprog"
RTC: Enabling Error Checking...
RTC: Running program...
t@1 (l@1) stopped in main at 0x0805136d
0x0805136d: main       :        pushl    %ebp
(dbx) when dlopen libjvm { suppress all in libjvm.so; }
(2) when dlopen libjvm { suppress all in libjvm.so; }  
(dbx) when dlopen libjava { suppress all in libjava.so; }
(3) when dlopen libjava { suppress all in libjava.so; }  
(dbx) cont                                             
Reading libjvm.so
Reading libsocket.so.1
Reading libsched.so.1
Reading libCrun.so.1
Reading libm.so.1
Reading libnsl.so.1
Reading libmd5.so.1
Reading libmp.so.2
Reading libhpi.so
Reading libverify.so
Reading libjava.so
Reading libzip.so
Reading en_US.ISO8859-1.so.3
hello world
hello world
Checking for memory leaks...

Actual leaks report    (actual leaks:           27  total size:      46851 bytes)

  Total     Num of  Leaked     Allocation call stack
  Size      Blocks  Block
                    Address
==========  ====== =========== =======================================
     44376       4      -      calloc < zcalloc 
      1072       1  0x8151c70  _nss_XbyY_buf_alloc < get_pwbuf < _getpwuid <
                               GetJavaProperties < Java_java_lang_System_initProperties <
                               0xa740a89a< 0xa7402a14< 0xa74001fc
       814       1  0x8072518  MemAlloc < CreateExecutionEnvironment < main 
       280      10      -      operator new < Thread::Thread 
       102       1  0x8072498  _strdup < CreateExecutionEnvironment < main 
        56       1  0x81697f0  calloc < Java_java_util_zip_Inflater_init < 0xa740a89a<
                               0xa7402a6a< 0xa7402aeb< 0xa7402a14< 0xa7402a14< 0xa7402a14
        41       1  0x8072bd8  main 
        30       1  0x8072c58  SetJavaCommandLineProp < main 
        16       1  0x806f180  _setlocale < GetJavaProperties <
                               Java_java_lang_System_initProperties < 0xa740a89a< 0xa7402a14<
                               0xa74001fc< JavaCalls::call_helper < os::os_exception_wrapper 
        12       1  0x806f2e8  operator new < instanceKlass::add_dependent_nmethod <
                               nmethod::new_nmethod < ciEnv::register_method <
                               Compile::Compile #Nvariant 1 < C2Compiler::compile_method <
                               CompileBroker::invoke_compiler_on_method <
                               CompileBroker::compiler_thread_loop 
        12       1  0x806ee60  CheckJvmType < CreateExecutionEnvironment < main 
        12       1  0x806ede8  MemAlloc < CreateExecutionEnvironment < main 
        12       1  0x806edc0  main 
         8       1  0x8071cb8  _strdup < ReadKnownVMs < CreateExecutionEnvironment < main 
         8       1  0x8071cf8  _strdup < ReadKnownVMs < CreateExecutionEnvironment < main 

この出力は、プロセスが終了しようとしている時点でメモリーが解放されない場合、dbxデバッガがメモリー・リークを報告することを示しています。ただし、初期化時に割り当てられ、プロセスが終了するまで必要となるメモリーは、多くの場合、ネイティブ・コードでは解放されません。したがって、そのような場合は、dbxデバッガが実際にはリークではないメモリー・リークを報告することがあります。

注意:

前の例では、仮想マシンlibjvm.soとJavaサポート・ライブラリlibjava.soで報告されたリークを抑制するために、2つのsuppressコマンドを使用しています。

libumemツールによるリークの検出

libumem.soライブラリとモジュラ・デバッガmdbは、最初にOracle Solaris 9オペレーティング・システムupdate 3で導入され、メモリー・リークをデバッグするために使用できます。

libumemを使用する前に、次の例に示すように、libumemライブラリをプリロードし、環境変数を設定する必要があります。

$ LD_PRELOAD=libumem.so
$ export LD_PRELOAD
$ UMEM_DEBUG=default
$ export UMEM_DEBUG

ここで、Javaアプリケーションを実行しますが、それを終了する前に停止します。次の例では、_exitシステム・コールが呼び出されたときにtrussを使用してプロセスを停止しています。

$ truss -f -T _exit java MainClass arguments

この時点で、次の例に示すように、mdbデバッガを接続できます。

$ mdb -p pid
>::findleaks

::findleaksコマンドは、mdbでメモリー・リークを検出するコマンドです。リークが検出されたら、このコマンドによって割当て呼出しのアドレス、バッファ・アドレス、および最も近いシンボルを出力します。

bufctl構造体をダンプして、メモリー・リークが発生した割当てのスタック・トレースを取得することもできます。この構造体のアドレスは、::findleaksコマンドの出力から取得できます。

メモリー・リークの原因のトラブルシューティングについては、libumemを使用したメモリー・リークの分析を参照してください。