アプリケーションの実行時間がしだいに長くなる場合や、オペレーティング・システムの実行速度がしだいに低下しているように思われる場合、これはメモリー・リークの兆候である可能性があります。つまり、仮想メモリーが割り当てられているけれども、それが不要になったときに返されません。最終的には、アプリケーションまたはシステムがメモリーを使い果たし、アプリケーションが異常終了します。
この章の構成は、次のとおりです。
Java Flight Recorder (JFR)は商用機能です。開発者のデスクトップまたはラップトップ上では無償で、また評価目的であればテスト環境、開発環境および本番環境で使用できます。
ただし、JFRを本番サーバーで有効にする場合は、商用ライセンスが必要です。それ以外の目的でJava Mission Control (JMC)をJDKで使用する場合、商用ライセンスは必要ではありません。
JFRの商用機能と入手方法の詳細は、製品マニュアルを参照してください。
JFRの商用ライセンスの詳細は、ライセンス契約を参照してください。
次の項では、図を示して、Javaフライト・レコーダでメモリー・リークをデバッグする方法を説明します。
Javaフライト記録を使用してメモリー・リークを早期に検出し、OutOfmemoryErrors
を防止します。
スロー・メモリー・リークの検出は難しい場合があります。一般的な症状は、頻繁なガベージ・コレクションにより実行に時間がかかり、その後、アプリケーションが低速になります。やがてOutOfmemoryErrors
が表示される可能性があります。ただし、Javaフライト・レコーダを使用すると、問題が発生する前であっても、メモリー・リークを早期に検出できます。
アプリケーションのライブ・セットの経時的な増加がないか確認します。ライブ・セットは、Oldコレクションの後(すべての非ライブ・オブジェクトがガベージ・コレクトされた後)に使用されるJavaヒープ容量です。ライブ・セットは、様々な方法で調査できます。たとえば、-verbosegc
オプションを使用して実行するか、JMC JMXコンソールを使用してJVMに接続し、com.sun.management.GarbageCollectorAggregator MBean
を見ます。しかし、別の簡単なアプローチは、フライト記録を取ることです。
記録を開始するときにヒープ統計を有効にすると、記録の開始時と終了時にOldコレクションがトリガーされます。これにより、アプリケーションに若干の遅延が生じる場合があります。しかし、ヒープ統計は、正確なライブ・セット情報を生成します。やや急速なメモリー・リークが疑われる場合は、たとえば、1時間以上実行されるプロファイリング記録を取ります。図3-1に示すように、「メモリー」タブをクリックし、「ガベージ・コレクション」タブを選択して、最初と最後のOldコレクションを確認します。
図3-1,に示すように、最初のOldコレクションを選択して、GC後のヒープ・データとヒープ使用量を調べます。この記録では、34.10MBです。今度は、最後のOldコレクションからの同じデータを調べて、ライブ・セットが増加したかどうかを確認します。記録を取る前に、アプリケーションが起動し、安定した状態に到達できるようにする必要があります。
ゆっくりしたリークの場合は、5分間の短時間の記録を取ることができます。次に、たとえば、(メモリー・リークの速さに応じて)24時間後に、もう1回記録を取ります。明らかに、ライブ・セットは上下しますが、時間の経過とともに着実に増加している場合は、メモリー・リークの可能性があります。
Javaフライト記録を使用して、メモリー・リークを特定します。
記録がリークを示したら、「オブジェクト統計」を調べます。1つの長い記録を検討し、記録を通してヒープ使用量が最も増加したクラスを調べます。間隔を置いて複数の記録を取った場合は、図3-2に示すように、「ヒープの内容」セクションを比較し、どのオブジェクト・タイプが記録間で最も増加しているかを調べます。
特に、標準ライブラリの一部ではないクラスに注目してください。たとえば、多くの場合、最大の増加率の1つとしてChar
配列が表示されます。これは、多くのStrings
が割り当てられているためです。したがって、これらのStrings
を維持しているオブジェクトに注意します。10個のStrings
をメンバーとして持つクラスがある場合、オブジェクト自体がヒープを使いすぎることはありません。ヒープは、主にChar
配列へのポインタが含まれているStrings
によって使用されます。したがって、オブジェクトのサイズではなく、インスタンスの数でソートするのが適切です。いずれかのアプリケーション・クラスに多くのインスタンスがある場合、それらのオブジェクトが他のオブジェクトを維持している可能性があります。
Javaフライト記録を使用した追加情報を使用してメモリー・リークを特定するためのヒント。
Javaフライト記録を使用して、いくつかの追加情報を得ることができます。
図3-3に示すように、「割当て」サブタブで、オブジェクトの割当先のサンプルを見ます。
特定のクラスのリークが予想される場合は、「新しいTLABの割当て」タブを見てください。割り当てられているクラス・サンプルをチェックします。スロー・リークの場合は、このオブジェクトの割当てが少なく、サンプルがないこともあります。また、特定の割当てサイトのみがリークの原因となっている可能性もあります。要約すると、これによってリークの適切な割当てスタック・トレースに到達することは保証されませんが、重要な手がかりを得る可能性があります。
java.lang.OutOfMemoryError
のエラーは、Javaヒープにオブジェクトを割り当てるための十分な空間がないときにスローされます。
メモリー・リークの一般的な兆候の1つは、java.lang.OutOfMemoryError
例外です。この場合、ガベージ・コレクタによって新しいオブジェクトを格納するための空間を確保することも、ヒープをこれ以上拡張することもできません。また、このエラーは、Javaクラスのロードをサポートするための十分なネイティブ・メモリーがないときにスローされる場合もあります。ガベージ・コレクションの実行に過剰な時間が消費され、メモリーがほとんど解放されていない場合、まれにjava.lang.OutOfMemoryError
がスローされることがあります。
java.lang.OutOfMemoryError
例外がスローされるときに、スタック・トレースも出力されます。
java.lang.OutOfMemoryError
例外は、ネイティブ割当てを満たすことができないとき(たとえば、スワップ空間が少ない場合)に、ネイティブ・ライブラリ・コードによってスローされることもあります。
OutOfMemoryError
例外を診断する最初の手順は、その例外の原因を突き止めることです。スローされた理由が、Javaヒープがいっぱいであるからなのか、それともネイティブ・ヒープがいっぱいであるからなのかを調べます。原因を見つけることができるように、例外テキストの最後に詳細なメッセージが含まれています(次の例外を参照)。
原因: Java heap spaceという詳細メッセージは、Javaヒープ内でオブジェクトを割り当てることができなかったことを示しています。このエラーは、必ずしもメモリー・リークを意味しません。この問題は、指定されたヒープ・サイズ(指定されていない場合はデフォルト・サイズ)がアプリケーションにとって十分でないという、単純な構成の問題である可能性があります。
それ以外の場合、特に長期にわたって稼動するアプリケーションでは、このメッセージはアプリケーションが誤ってオブジェクトへの参照を保持しているため、オブジェクトのガベージ・コレクションができないことを示している可能性があります。これは、Java言語ではメモリー・リークに相当します。注意: アプリケーションによって呼び出されたAPIが誤ってオブジェクト参照を保持している可能性もあります。
このエラーのもう1つの潜在的な原因は、ファイナライザを過度に使用するアプリケーションで発生します。クラスにfinalize
メソッドがある場合、そのタイプのオブジェクトの空間はガベージ・コレクション時に再利用されません。かわりに、ガベージ・コレクション後にファイナライズ用のキューにオブジェクトが入れられ、後でファイナライズが実行されます。Oracle Sunの実装では、ファイナライザはファイナライズ・キューを提供するデーモン・スレッドによって実行されます。ファイナライザ・スレッドがファイナライズ・キューを処理しきれなかった場合は、Javaヒープがいっぱいになる可能性があり、このタイプのOutOfMemoryError
例外がスローされます。この状況を発生させるシナリオの1つは、アプリケーションが優先度の高いスレッドを作成しているため、ファイナライザ・スレッドがファイナライズ・キューを処理する速度よりキューの増加速度の方が速くなっている場合です。
原因: 「GC overhead limit exceeded」という詳細メッセージは、ガベージ・コレクタが常時実行されているため、Javaプログラムの処理がほとんど進んでいないことを示しています。ガベージ・コレクション後、Javaプロセスでガベージ・コレクションの実行に約98%を超える時間が費やされ、ヒープのリカバリが2%未満であり、その状態が直近5回(コンパイル時間定数)の連続するガベージ・コレクションで続いている場合に、java.lang.OutOfMemoryError
がスローされます。この例外は通常、Javaヒープに新しい割当て用の空き領域がわずかしかなく、ライブ・データの容量をほとんど格納できないためにスローされます。
原因: 「Requested array size exceeds VM limit」という詳細メッセージは、アプリケーション(またはそのアプリケーションが使用するAPI)がヒープ・サイズより大きい配列を割り当てようとしたことを示しています。たとえば、アプリケーションが512MBの配列を割り当てようとしたが、最大ヒープ・サイズが256MBだった場合は、「Requested array size exceeds VM limit」という理由とともにOutOfMemoryError
がスローされます。
原因: Javaクラス・メタデータ(Javaクラスの仮想マシン内部表現)がネイティブ・メモリー(ここでは、メタスペースと呼ばれる)に割り当てられています。クラス・メタデータのメタスペースが枯渇すると、「MetaSpace
」という詳細のjava.lang.OutOfMemoryError
例外がスローされます。クラス・メタデータに使用できるメタスペース容量は、コマンド行に指定するMaxMetaSpaceSize
パラメータにより制限されます。クラス・メタデータに必要なネイティブ・メモリー容量がMaxMetaSpaceSize
を超過すると、MetaSpace
詳細とともにjava.lang.OutOfMemoryError
例外がスローされます。
原因: 「request size bytes for reason.Out of swap space?」という詳細メッセージは、OutOfMemoryError
例外であるように見えます。しかし、ネイティブ・ヒープからの割当てが失敗し、ネイティブ・ヒープが枯渇寸前になっている可能性があるときに、Java HotSpot VMコードはこの見かけ上の例外を報告します。このメッセージは、失敗した要求のサイズ(バイト数)とメモリー要求の理由を示しています。通常、その理由は割当ての失敗を報告するソース・モジュールの名前になりますが、場合によっては実際の理由になることもあります。
原因: 64ビット・プラットフォーム上で、クラス・メタデータへのポインタが32ビットのオフセットで表現されている可能性があります(UseCompressedOops
を使用)。これはコマンド行フラグUseCompressedClassPointers
で制御されます(デフォルトの場合)。UseCompressedClassPointers
が使用されている場合、クラス・メタデータに使用できる領域量はCompressedClassSpaceSize
の容量で固定されます。UseCompressedClassPointers
に必要な領域がCompressedClassSpaceSize
を超える場合、「Compressed class space」という詳細のjava.lang.OutOfMemoryError
がスローされます。
原因: エラー・メッセージの詳細部分が「reason stack_trace_with_native_method」であり、最上位フレームがネイティブ・メソッドであるスタック・トレースが出力される場合、これはネイティブ・メソッドで割当て失敗が発生したことを示しています。これと前のメッセージとの違いは、割当ての失敗がJVMコードではなく、Java Native Interface (JNI)またはネイティブ・メソッドで検出されたことです。
致命的エラー・ログやクラッシュ・ダンプの情報を使用して、クラッシュをトラブルシューティングします。
ネイティブ・ヒープからの割当てが失敗した直後に、アプリケーションがクラッシュすることがあります。これは、メモリー割当て関数によって返されたエラーをチェックしないネイティブ・コードで発生します。
たとえば、malloc
システム・コールは使用可能なメモリーがない場合にnull
を返します。malloc
からの戻り値がチェックされない場合、アプリケーションは無効なメモリー位置にアクセスしようとしてクラッシュする可能性があります。状況によっては、このタイプの問題を特定することが難しいことがあります。
しかし、致命的エラー・ログやクラッシュ・ダンプの情報があれば、この問題を十分に診断できる場合もあります。致命的エラー・ログについては、「致命的エラー・ログ」で詳しく説明されています。割当ての失敗によってクラッシュが生じた場合は、割当ての失敗の理由を調べてください。ネイティブ・ヒープに関する他の問題と同様に、システムに十分な容量のスワップ空間が構成されていなかったり、システム上の別のプロセスがすべてのメモリー・リソースを消費していたり、システムのメモリー不足を引き起こすリークがアプリケーション内(またはそこから呼び出されたAPI内)で発生していたりする可能性があります。
NetBeansプロファイラを使用してJava言語コードのリークを診断します。
Java言語コード内のリークの診断は難しい可能性があります。通常、アプリケーションの非常に詳しい知識が必要です。さらに、プロセスが反復し、冗長なことがよくあります。この項では、Java言語コード内のメモリー・リークを診断するために使用できるツールについて説明します。
注意:
この項で説明したツールに加えて、数多くのサードパーティ・メモリー・デバッガ・ツールが利用可能です。メモリー・デバッグ機能を備えた商用ツールの例として、Eclipse Memory Analyzer Tool (MAT)とYourKit (www.yourkit.com)の2つがあります。他にも数多くありますが、推奨される特定の製品はありません。
Java言語コードのリークの診断に使用されるユーティリティを次に示します。
次の項では、Java言語コードでのリークを診断するためのその他の方法について説明します。
メモリー・リークを特定するためのヒープ・ヒストグラムは、様々なコマンドやオプションを使用して取得できます。
ヒープ・ヒストグラムを調べることで、メモリー・リークをすばやく絞り込んでみることができます。ヒープ・ヒストグラムはいくつかの方法で取得できます。
ファイナライズが保留されているオブジェクトをモニターするには、様々なコマンドやオプションを使用できます。
OutOfMemoryError例外が「Java heap space」という詳細メッセージとともにスローされた場合、その原因はファイナライザの過度の使用によるものである可能性があります。これを診断するために、ファイナライズを保留しているオブジェクトの数をモニターするためのオプションが複数用意されています。
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ライブラリ用の簡単な局所的割当て追跡方法です。まず、すべてのソース・ファイルに次の行を定義します。
#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()などが使用されていた場合は、これらに対しても同じような関数を作成する必要があります。
より大域的な方法でネイティブ・ヒープのメモリー・リークを検出するには、プロセス全体のライブラリ呼び出しへの介入が必要になります。
ツールを使用して、オペレーティング・システムのメモリー割当てを追跡できます。
ほとんどのオペレーティング・システムには、なんらかの形式の大域的割り当て追跡サポートが含まれています。
/Md
および/Mdd
コンパイラ・オプションがあります。mtrace
やlibnjamd
などのツールがあります。watchmalloc
ツールが用意されています。Oracle Solaris 9オペレーティング・システムUpdate 3では、libumem
ツールも提供されています。「libumemツールによるリークの検出」を参照してください。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.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
を使用したメモリー・リークの分析を参照してください。