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

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

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

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

JDK Mission Controlを使用したメモリー・リークのデバッグ

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

次の項では、JMCでフライト記録を分析してメモリー・リークをデバッグする方法について説明します。

メモリー・リークの検出

JMCを使用してメモリー・リークを早期に検出し、OutOfmemoryErrorsを防止できます。

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

アプリケーションのライブ・セットの経時的な増加がないか確認します。ライブ・セットは、Oldコレクションの後(すべての非ライブ・オブジェクトがガベージ・コレクトされた後)に使用されるJavaヒープ容量です。ライブ・セットを検査するには、JMCを開き、Java管理コンソール(JMX)を使用してJVMに接続します。「MBeanブラウザ」タブを開き、com.sun.managementの下のGarbageCollectorAggregator MBeanを探します。

JMCを開き、1時間分の一定時間の記録(プロファイリング記録)を開始します。フライト記録を開始する前に、「メモリー・リーク検出」設定から「オブジェクト・タイプ+割当てスタック・トレース+ GCルートへのパス」オプションが選択されていることを確認してください。

記録が完了すると、JMCで記録ファイル(.jfr)が開きます。「自動分析結果」ページを参照してください。メモリー・リークを検出するには、ページの「ライブ・オブジェクト」セクションにフォーカスを置きます。次に、ヒープ・サイズの問題を示す記録のサンプル図を示します:

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

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

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

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

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

図3-2の説明が続く
「図3-2メモリー・リーク-「メモリー」ページ」の説明

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

リーク・クラスの調査

Javaフライト記録を使用して、リーク・クラスを識別できます。

リーク・クラスを検索するには、「メモリー」ページを開き、「ライブ・オブジェクト」ページをクリックします。次に、リーク・クラスを示す記録のサンプル図を示します。

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

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

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

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

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

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

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

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

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: Requested array size exceeds VM limit

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

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

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.スワップ領域が不足していますか。

原因: 「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は、このようなオブジェクトが一般的に示すメモリーの割り当てと再利用のパターンを使用します。このプロセスには、メモリー再利用の欠落も含まれます。このプロファイラは、これらのオブジェクトがどこで割り当てられたかをチェックできます(リークの根本原因を特定するには、通常はこれで十分です)。

次の項では、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 --corecore_file
    

    たとえば、アプリケーションの実行中に-XX:+CrashOnOutOfMemoryErrorコマンドライン・オプションを指定すると、OutOfMemoryError例外がスローされたときに、JVMによってコア・ダンプが生成されます。その後、次の例に示すように、コア・ファイルに対して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ドキュメントへのリンクおよびコード例は、「カスタム診断ツール」に記載されています。コード例を拡張して、ファイナライズ保留中の数の報告を簡単に追加できます。
ファイナライズの検出とファイナライズからの移行の詳細は、『Java Platform, Standard Edition HotSpot仮想マシン・ガベージ・コレクション・チューニング・ガイド』「ファイナライズと弱参照、ソフト参照およびファントム参照」を参照してください。

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

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

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

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

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

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

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を使用したメモリー・リークの分析を参照してください。