Javaメモリリークのハンティング
公開: 2022-03-11経験の浅いプログラマーは、Javaの自動ガベージコレクションにより、メモリ管理について心配する必要が完全になくなると考えることがよくあります。 これはよくある誤解です。ガベージコレクターは最善を尽くしますが、最高のプログラマーでさえ、メモリリークを壊滅させる犠牲になる可能性があります。 説明させてください。
不要になったオブジェクト参照が不必要に維持されると、メモリリークが発生します。 これらのリークは悪いです。 1つは、プログラムがますます多くのリソースを消費するため、マシンに不必要なプレッシャーをかけることです。 さらに悪いことに、これらのリークの検出は困難な場合があります。静的分析では、これらの冗長な参照を正確に特定するのに苦労することが多く、既存のリーク検出ツールは、個々のオブジェクトに関する詳細な情報を追跡および報告するため、解釈が難しく、精度に欠ける結果が生成されます。
言い換えれば、リークは特定するのが難しすぎるか、特定性が高すぎて役に立たないという観点から特定されます。
実際には、類似した重複する症状を伴うが、さまざまな原因と解決策を伴う記憶の問題の4つのカテゴリーがあります。
パフォーマンス:通常、過度のオブジェクトの作成と削除、ガベージコレクションの長い遅延、過度のオペレーティングシステムページスワッピングなどに関連します。
リソースの制約:使用可能なメモリが少ないか、メモリが断片化されすぎて大きなオブジェクトを割り当てることができない場合に発生します。これは、ネイティブであるか、より一般的にはJavaヒープ関連である可能性があります。
Javaヒープリーク:Javaオブジェクトが解放されずに継続的に作成される、従来のメモリリーク。 これは通常、潜在オブジェクト参照が原因で発生します。
ネイティブメモリリーク:JNIコード、ドライバー、さらにはJVM割り当てによって行われた割り当てなど、Javaヒープの外部で継続的に増加するメモリ使用率に関連します。
このメモリ管理チュートリアルでは、Javaヒープリークに焦点を当て、 Java VisualVMレポートに基づいてそのようなリークを検出し、実行中にJavaテクノロジベースのアプリケーションを分析するためのビジュアルインターフェイスを利用するアプローチの概要を説明します。
ただし、メモリリークを防止して見つける前に、それらが発生する方法と理由を理解する必要があります。 (注:メモリリークの複雑さをうまく処理している場合は、先にスキップできます。 )
メモリリーク:入門書
手始めに、メモリリークを病気と考え、JavaのOutOfMemoryError
(OOM、簡潔にするため)を症状と考えてください。 ただし、他の病気と同様に、すべてのOOMが必ずしもメモリリークを意味するわけではありません。OOMは、多数のローカル変数または他のそのようなイベントの生成が原因で発生する可能性があります。 一方、特にデスクトップアプリケーションやクライアントアプリケーション(再起動せずに長時間実行されない)の場合は、必ずしもすべてのメモリリークがOOMとして現れるわけではありません。
なぜこれらのリークはそれほどひどいのですか? 特に、プログラムの実行中にメモリのブロックがリークすると、システムのパフォーマンスが時間の経過とともに低下することがよくあります。システムに空き物理メモリがなくなると、割り当てられているが未使用のメモリブロックをスワップアウトする必要があるためです。 最終的に、プログラムは利用可能な仮想アドレス空間を使い果たし、OOMにつながる可能性があります。
OutOfMemoryError
の解読
上記のように、OOMはメモリリークの一般的な兆候です。 基本的に、新しいオブジェクトを割り当てるのに十分なスペースがない場合、エラーがスローされます。 ガベージコレクターが必要なスペースを見つけることができず、ヒープをそれ以上拡張できません。 したがって、スタックトレースとともにエラーが発生します。
OOMを診断する最初のステップは、エラーが実際に何を意味するかを判断することです。 これは明白に聞こえますが、答えは必ずしも明確ではありません。 例:Javaヒープがいっぱいであるため、またはネイティブヒープがいっぱいであるために、OOMが表示されていますか? この質問に答えるために、考えられるエラーメッセージのいくつかを分析してみましょう。
java.lang.OutOfMemoryError: Java heap space
java.lang.OutOfMemoryError: PermGen space
java.lang.OutOfMemoryError: Requested array size exceeds VM limit
java.lang.OutOfMemoryError: request <size> bytes for <reason>. Out of swap space?
java.lang.OutOfMemoryError: <reason> <stack trace> (Native method)
「Javaヒープスペース」
このエラーメッセージは、必ずしもメモリリークを意味するわけではありません。 実際、問題は構成の問題と同じくらい単純な場合があります。
たとえば、このタイプのOutOfMemoryError
を一貫して生成しているアプリケーションの分析を担当しました。 調査の結果、原因はアレイのインスタンス化であり、メモリを大量に要求していることがわかりました。 この場合、それはアプリケーションの障害ではなく、アプリケーションサーバーがデフォルトのヒープサイズに依存していました。これは小さすぎました。 JVMのメモリパラメータを調整することで問題を解決しました。
その他の場合、特に長期間使用されるアプリケーションの場合、メッセージは、オブジェクトへの参照を意図せずに保持していることを示している可能性があり、ガベージコレクターがオブジェクトをクリーンアップできません。 これは、メモリリークに相当するJava言語です。 (注:アプリケーションによって呼び出されるAPIは、意図せずにオブジェクト参照を保持している可能性もあります。 )
これらの「Javaヒープスペース」OOMのもう1つの潜在的な原因は、ファイナライザーを使用することで発生します。 クラスにfinalize
メソッドがある場合、そのタイプのオブジェクトは、ガベージコレクション時にスペースが再利用されません。 代わりに、ガベージコレクションの後、オブジェクトはファイナライズのためにキューに入れられます。これは後で行われます。 Sunの実装では、ファイナライザーはデーモンスレッドによって実行されます。 ファイナライザスレッドがファイナライズキューに追いつかない場合、Javaヒープがいっぱいになり、OOMがスローされる可能性があります。
「PermGenスペース」
このエラーメッセージは、永続的な生成がいっぱいであることを示しています。 永続世代は、クラスオブジェクトとメソッドオブジェクトを格納するヒープの領域です。 アプリケーションが多数のクラスをロードする場合は、 -XX:MaxPermSize
オプションを使用して永続世代のサイズを増やす必要がある場合があります。
インターンされたjava.lang.String
オブジェクトも、永続世代に格納されます。 java.lang.String
クラスは、文字列のプールを維持します。 インターンメソッドが呼び出されると、メソッドはプールをチェックして、同等の文字列が存在するかどうかを確認します。 その場合、internメソッドによって返されます。 そうでない場合、文字列はプールに追加されます。 より正確には、 java.lang.String.intern
メソッドは文字列の正規表現を返します。 結果は、その文字列がリテラルとして表示された場合に返される同じクラスインスタンスへの参照です。 アプリケーションが多数の文字列をインターンする場合は、永続世代のサイズを増やす必要がある場合があります。
注: jmap -permgen
コマンドを使用して、内部化されたStringインスタンスに関する情報など、永続的な生成に関連する統計を出力できます。
「要求されたアレイサイズがVM制限を超えています」
このエラーは、アプリケーション(またはそのアプリケーションで使用されるAPI)がヒープサイズよりも大きい配列を割り当てようとしたことを示しています。 たとえば、アプリケーションが512MBのアレイを割り当てようとしたが、最大ヒープサイズが256MBの場合、このエラーメッセージとともにOOMがスローされます。 ほとんどの場合、問題は構成の問題か、アプリケーションが大規模なアレイを割り当てようとしたときに発生するバグのいずれかです。
「<reason>の<size>バイトを要求します。 スワップスペースが足りませんか?」
このメッセージはOOMのようです。 ただし、ネイティブヒープからの割り当てが失敗し、ネイティブヒープが使い果たされる可能性がある場合、HotSpotVMはこの明らかな例外をスローします。 メッセージには、失敗した要求のサイズ(バイト単位)とメモリ要求の理由が含まれています。 ほとんどの場合、<reason>は、割り当ての失敗を報告しているソースモジュールの名前です。
このタイプのOOMがスローされた場合、問題をさらに診断するために、オペレーティングシステムでトラブルシューティングユーティリティを使用する必要がある場合があります。 場合によっては、問題がアプリケーションに関連していないこともあります。 たとえば、次の場合にこのエラーが発生する可能性があります。
オペレーティングシステムが不十分なスワップスペースで構成されています。
システム上の別のプロセスは、使用可能なすべてのメモリリソースを消費しています。
ネイティブリークが原因でアプリケーションが失敗した可能性もあります(たとえば、アプリケーションまたはライブラリコードの一部がメモリを継続的に割り当てているが、オペレーティングシステムへの解放に失敗した場合)。
<理由><スタックトレース>(ネイティブメソッド)
このエラーメッセージが表示され、スタックトレースの最上位フレームがネイティブメソッドである場合、そのネイティブメソッドで割り当てエラーが発生しています。 このメッセージと前のメッセージの違いは、Javaメモリ割り当ての失敗がJavaVMコードではなくJNIまたはネイティブメソッドで検出されたことです。
このタイプのOOMがスローされた場合、問題をさらに診断するためにオペレーティングシステムのユーティリティを使用する必要がある場合があります。
OOMなしでアプリケーションがクラッシュする
場合によっては、ネイティブヒープからの割り当てが失敗した直後にアプリケーションがクラッシュすることがあります。 これは、メモリ割り当て関数によって返されるエラーをチェックしないネイティブコードを実行している場合に発生します。
たとえば、使用可能なメモリがない場合、 malloc
システムコールはNULL
を返します。 malloc
からの戻りがチェックされていない場合、無効なメモリ位置にアクセスしようとするとアプリケーションがクラッシュする可能性があります。 状況によっては、このタイプの問題を見つけるのが難しい場合があります。
場合によっては、致命的なエラーログまたはクラッシュダンプからの情報で十分です。 クラッシュの原因が一部のメモリ割り当てでのエラー処理の欠如であると判断された場合は、その割り当ての失敗の理由を突き止める必要があります。 他のネイティブヒープの問題と同様に、システムが不十分なスワップスペースで構成されている場合や、別のプロセスが使用可能なすべてのメモリリソースを消費している場合などがあります。
リークの診断
ほとんどの場合、メモリリークを診断するには、問題のアプリケーションに関する非常に詳細な知識が必要です。 警告:プロセスは長く、反復する可能性があります。
メモリリークを追跡するための戦略は比較的簡単です。
症状を特定する
詳細なガベージコレクションを有効にする
プロファイリングを有効にする
トレースを分析する
1.症状を特定する
説明したように、多くの場合、Javaプロセスは最終的にOOMランタイム例外をスローします。これは、メモリリソースが使い果たされたことを明確に示します。 この場合、通常のメモリの枯渇とリークを区別する必要があります。 OOMのメッセージを分析し、上記の議論に基づいて犯人を見つけようとします。
多くの場合、Javaアプリケーションがランタイムヒープが提供するよりも多くのストレージを要求する場合、それは不十分な設計が原因である可能性があります。 たとえば、アプリケーションが画像の複数のコピーを作成したり、ファイルを配列にロードしたりする場合、画像またはファイルが非常に大きいと、ストレージが不足します。 これは通常のリソースの枯渇です。 アプリケーションは設計どおりに機能しています(ただし、この設計は明らかに骨の折れるものです)。
ただし、アプリケーションが同じ種類のデータを処理しているときにメモリ使用率を着実に増加させると、メモリリークが発生する可能性があります。
2.VerboseGarbageCollectionを有効にします
実際にメモリリークが発生していることを確認する最も簡単な方法の1つは、詳細なガベージコレクションを有効にすることです。 通常、メモリ制約の問題は、 verbosegc
出力のパターンを調べることで特定できます。
具体的には、 -verbosegc
引数を使用すると、ガベージコレクション(GC)プロセスが開始されるたびにトレースを生成できます。 つまり、メモリがガベージコレクションされると、要約レポートが標準エラーで出力され、メモリがどのように管理されているかがわかります。
–verbosegc
オプションで生成される典型的な出力は次のとおりです。
このGCトレースファイルの各ブロック(またはスタンザ)には、昇順で番号が付けられています。 このトレースを理解するには、連続するAllocation Failureスタンザを調べ、解放されたメモリ(バイトとパーセンテージ)が時間の経過とともに減少し、合計メモリ(ここでは19725304)が増加していることを確認する必要があります。 これらは、メモリ枯渇の典型的な兆候です。

3.プロファイリングを有効にする
JVMが異なれば、ヒープアクティビティを反映するトレースファイルを生成するさまざまな方法が提供されます。これには通常、オブジェクトのタイプとサイズに関する詳細情報が含まれます。 これは、ヒープのプロファイリングと呼ばれます。
4.トレースを分析します
この投稿では、JavaVisualVMによって生成されたトレースに焦点を当てています。 トレースは、さまざまなJavaメモリリーク検出ツールで生成できるため、さまざまな形式で提供されますが、その背後にある考え方は常に同じです。ヒープ内に存在してはならないオブジェクトのブロックを見つけ、これらのオブジェクトが蓄積されているかどうかを判断します。リリースする代わりに。 特に興味深いのは、Javaアプリケーションで特定のイベントがトリガーされるたびに割り当てられることがわかっている一時的なオブジェクトです。 少量でしか存在しないはずのオブジェクトインスタンスが多数存在する場合は、通常、アプリケーションのバグを示しています。
最後に、メモリリークを解決するには、コードを徹底的に確認する必要があります。 オブジェクトのリークの種類を知ることは非常に役立ち、デバッグを大幅にスピードアップできます。
ガベージコレクションはJVMでどのように機能しますか?
メモリリークの問題があるアプリケーションの分析を開始する前に、まず、JVMでガベージコレクションがどのように機能するかを見てみましょう。
JVMは、トレースコレクターと呼ばれるガベージコレクターの形式を使用します。これは、基本的に、周囲の世界を一時停止し、すべてのルートオブジェクト(実行中のスレッドによって直接参照されるオブジェクト)をマークし、それらの参照に従って、途中で表示される各オブジェクトをマークすることによって動作します。
Javaは、世代別仮説の仮定に基づいて世代別ガベージコレクターと呼ばれるものを実装します。これは、作成されたオブジェクトの大部分がすぐに破棄され、すぐに収集されないオブジェクトはしばらくの間存在する可能性が高いというものです。
この仮定に基づいて、Javaはオブジェクトを複数の世代に分割します。 視覚的な解釈は次のとおりです。
若い世代-これはオブジェクトが始まるところです。 2つのサブ世代があります。
EdenSpace-オブジェクトはここから始まります。 ほとんどのオブジェクトは、エデンスペースで作成および破棄されます。 ここで、GCは、最適化されたガベージコレクションであるマイナーGCを実行します。 マイナーGCが実行されると、まだ必要なオブジェクトへの参照は、サバイバースペースの1つ(S0またはS1)に移行されます。
サバイバースペース(S0およびS1) -エデンを生き残ったオブジェクトはここに到達します。 これらは2つあり、常に1つだけが使用されています(重大なメモリリークがない限り)。 1つは空として指定され、もう1つはライブとして指定され、GCサイクルごとに交互に行われます。
Tenured Generation-旧世代(図2の旧スペース)とも呼ばれるこのスペースは、寿命の長い古いオブジェクトを保持します(十分に長生きする場合は、サバイバースペースから移動します)。 このスペースがいっぱいになると、GCはフルGCを実行しますが、これはパフォーマンスの点でより多くのコストがかかります。 このスペースが際限なく大きくなると、JVMは
OutOfMemoryError - Java heap space
をスローします。永続世代-永続世代と密接に関連する第3世代である永続世代は、Java言語レベルで同等性を持たないオブジェクトを記述するために仮想マシンが必要とするデータを保持するため、特別です。 たとえば、クラスとメソッドを記述するオブジェクトは永続世代に格納されます。
Javaは、世代ごとに異なるガベージコレクションメソッドを適用できるほど賢いです。 若い世代は、 ParallelNewCollectorと呼ばれるトレースコピーコレクターを使用して処理されます。 このコレクターは世界を止めますが、若い世代は一般的に小さいので、一時停止は短いです。
JVM世代とそれらがどのように機能するかについての詳細は、JavaHotSpot仮想マシンのドキュメントのメモリ管理を参照してください。
メモリリークの検出
メモリリークを見つけて排除するには、適切なメモリリークツールが必要です。 Java VisualVMを使用して、このようなリークを検出して削除するときが来ました。
JavaVisualVMを使用したヒープのリモートプロファイリング
VisualVMは、Javaテクノロジベースのアプリケーションの実行中に詳細情報を表示するためのビジュアルインターフェイスを提供するツールです。
VisualVMを使用すると、ローカルアプリケーションおよびリモートホストで実行されているアプリケーションに関連するデータを表示できます。 JVMソフトウェアインスタンスに関するデータをキャプチャして、ローカルシステムに保存することもできます。
Java VisualVMのすべての機能を利用するには、Java Platform、Standard Edition(Java SE)バージョン6以降を実行する必要があります。
JVMのリモート接続の有効化
実稼働環境では、コードが実行される実際のマシンにアクセスするのが難しいことがよくあります。 幸い、Javaアプリケーションをリモートでプロファイリングできます。
まず、ターゲットマシンへのJVMアクセスを許可する必要があります。 これを行うには、次の内容のjstatd.all.policyというファイルを作成します。
grant codebase "file:${java.home}/../lib/tools.jar" { permission java.security.AllPermission; };
ファイルが作成されたら、次のように、jstatd-仮想マシンjstatデーモンツールを使用してターゲットVMへのリモート接続を有効にする必要があります。
jstatd -p <PORT_NUMBER> -J-Djava.security.policy=<PATH_TO_POLICY_FILE>
例えば:
jstatd -p 1234 -J-Djava.security.policy=D:\jstatd.all.policy
ターゲットVMでjstatdを起動すると、ターゲットマシンに接続して、メモリリークの問題があるアプリケーションをリモートでプロファイリングできます。
リモートホストへの接続
クライアントマシンでプロンプトを開き、 jvisualvm
と入力してVisualVMツールを開きます。
次に、VisualVMにリモートホストを追加する必要があります。 ターゲットJVMが有効になっているため、J2SE 6以降を搭載した別のマシンからのリモート接続が可能になるため、Java VisualVMツールを起動して、リモートホストに接続します。 リモートホストとの接続が成功すると、次のように、ターゲットJVMで実行されているJavaアプリケーションが表示されます。
アプリケーションでメモリプロファイラーを実行するには、サイドパネルでその名前をダブルクリックするだけです。
これでメモリアナライザの設定が完了したので、メモリリークの問題があるアプリケーションを調査してみましょう。これをMemLeakと呼びます。
MemLeak
もちろん、Javaでメモリリークを作成する方法はいくつかあります。 簡単にするために、 HashMap
のキーとなるクラスを定義しますが、equals()メソッドとhashcode()メソッドは定義しません。
HashMapは、Mapインターフェイスのハッシュテーブル実装であり、キーと値の基本概念を定義します。各値は一意のキーに関連付けられているため、特定のキーと値のペアのキーがすでに存在する場合HashMap、現在の値が置き換えられます。
キークラスがequals()
)メソッドとhashcode()
メソッドの正しい実装を提供することが必須です。 それらがなければ、良いキーが生成されるという保証はありません。
equals()
)メソッドとhashcode()
メソッドを定義しないことで、同じキーをHashMapに何度も追加し、必要に応じてキーを置き換える代わりに、HashMapは継続的に成長し、これらの同一のキーを識別できず、 OutOfMemoryError
をスローします。 。
MemLeakクラスは次のとおりです。
package com.post.memory.leak; import java.util.Map; public class MemLeak { public final String key; public MemLeak(String key) { this.key =key; } public static void main(String args[]) { try { Map map = System.getProperties(); for(;;) { map.put(new MemLeak("key"), "value"); } } catch(Exception e) { e.printStackTrace(); } } }
注:メモリリークは、14行目の無限ループが原因ではありません。無限ループはリソースの枯渇につながる可能性がありますが、メモリリークは発生しません。 equals()
)メソッドとhashcode()
メソッドを適切に実装していれば、HashMap内に要素が1つしかないため、無限ループでもコードは正常に実行されます。
(興味のある人のために、ここに(意図的に)リークを生成するいくつかの代替手段があります。)
JavaVisualVMの使用
Java VisualVMを使用すると、Javaヒープをメモリ監視し、その動作がメモリリークを示しているかどうかを識別できます。
これは、初期化直後のMemLeakのJavaヒープアナライザーのグラフィック表現です(さまざまな世代の説明を思い出してください)。
わずか30秒後、旧世代はほぼ満杯になります。これは、フルGCを使用しても、旧世代が増え続けていることを示しており、メモリリークの明らかな兆候です。
このリークの原因を検出する1つの手段は、ヒープダンプを備えたJava VisualVMを使用して生成された次の画像(クリックしてズーム)に示されています。 ここでは、Hashtable $ Entryオブジェクトの50%がヒープ内にあり、2行目がMemLeakクラスを指していることがわかります。 したがって、メモリリークは、 MemLeakクラス内で使用されるハッシュテーブルが原因で発生します。
最後に、若い世代と古い世代が完全にいっぱいになっているOutOfMemoryError
の直後のJavaヒープを観察します。
結論
症状はさまざまで再現が難しいため、メモリリークはJavaアプリケーションの解決が最も難しい問題の1つです。 ここでは、メモリリークを発見し、その原因を特定するための段階的なアプローチの概要を説明しました。 ただし、何よりも、エラーメッセージを注意深く読み、スタックトレースに注意してください。すべてのリークが、表示されるほど単純であるとは限りません。
付録
Java VisualVMに加えて、メモリリーク検出を実行できるツールが他にもいくつかあります。 多くのリークディテクタは、メモリ管理ルーチンへの呼び出しをインターセプトすることにより、ライブラリレベルで動作します。 たとえば、 HPROF
は、ヒープおよびCPUプロファイリング用にJava 2 Platform Standard Edition(J2SE)にバンドルされているシンプルなコマンドラインツールです。 HPROF
の出力は、直接分析することも、 JHAT
などの他のツールの入力として使用することもできます。 Java 2 Enterprise Edition(J2EE)アプリケーションを使用する場合、Websphereアプリケーションサーバー用のIBM Heapdumpsなど、より使いやすいヒープダンプアナライザーソリューションがいくつかあります。