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

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

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

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

フライト・レコーダを使用したメモリー・リークのデバッグ

フライト・レコーダは、JavaランタイムおよびJavaランタイムで実行されているJavaアプリケーションの詳細情報を記録します。この情報はメモリー・リークを特定するために使用できます。

スロー・メモリー・リークの検出は難しい場合があります。一般的な症状は、頻繁なガベージ・コレクションにより実行に時間がかかり、その後、アプリケーションが低速になります。やがてOutOfMemoryErrorsが表示される可能性があります。

メモリー・リークを検出するには、フライト・レコーダがリークの発生時点で実行されている必要があります。フライト・レコーダのオーバーヘッドは、1%未満と非常に低く、常に本番環境で安全に稼働するように設計されています。

次の例に示すように、アプリケーションがjavaコマンドを使用して起動されたときに記録を開始します。

$ java -XX:StartFlightRecording

JVMがメモリー不足になってOutMemoryErrorにより終了した場合、接頭辞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
  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
  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
  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要素を参照して、オブジェクトがいつ割り当てられたかを確認します。通常、起動時に割り当てられるオブジェクトはメモリー・リークではなく、ダンプの取得時の近くで割り当てられるオブジェクトでもありません。startTime要素は、ダンプが取得された時刻を示し、duration要素はダンプの所要時間を示します。

  • 次に、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ルートへの参照チェーンも提供されます。これは通常、メモリー・リークの原因を確認するために十分な情報です。

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

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

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

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

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

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

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

IBM Rational Purifyのようなツールを使用すると、ネイティブ・コードの通常の状況でこれらのリークを検出したり、初期化されていないメモリーへの割当てや解放されたメモリーへのアクセスを表すネイティブ・ヒープ・メモリーへのアクセスを検出したりできます。

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

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

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などのツールがあります。