経験の浅いプログラマーは、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 bytes for . Out of swap space?
java.lang.OutOfMemoryError: (Native method)
filetypetxtカードcvv2017
このエラーメッセージは、必ずしもメモリリークを意味するわけではありません。実際、問題は構成の問題と同じくらい単純な場合があります。
たとえば、私はこのタイプのOutOfMemoryError
を一貫して生成しているアプリケーションの分析を担当しました。調査の結果、原因は、メモリを大量に要求する配列のインスタンス化であることがわかりました。この場合、それはアプリケーションの障害ではなく、アプリケーションサーバーがデフォルトのヒープサイズに依存していたため、これは小さすぎました。私は調整することによって問題を解決しました JVMのメモリパラメータ 。
その他の場合、特に長期間使用されるアプリケーションの場合、メッセージは、意図せずに使用していることを示している可能性があります オブジェクトへの参照を保持する 、ガベージコレクタがそれらをクリーンアップするのを防ぎます。 これは、メモリリークに相当するJava言語です。 。 (( 注:アプリケーションによって呼び出されたAPIは、意図せずにオブジェクト参照を保持している可能性もあります。 )
これらの「Javaヒープスペース」OOMのもう1つの潜在的な原因は、 ファイナライザー 。クラスにfinalize
がある場合メソッドの場合、そのタイプのオブジェクトは、ガベージコレクション時にスペースが再利用されません。代わりに、ガベージコレクションの後、オブジェクトはファイナライズのためにキューに入れられます。これは後で発生します。 Sunの実装では、ファイナライザーは デーモンスレッド 。ファイナライザスレッドがファイナライズキューに追いつかない場合、Javaヒープがいっぱいになり、OOMがスローされる可能性があります。
このエラーメッセージは、 永久世代 一杯。永続世代は、クラスオブジェクトとメソッドオブジェクトを格納するヒープの領域です。アプリケーションが多数のクラスをロードする場合は、-XX:MaxPermSize
を使用して永続世代のサイズを増やす必要がある場合があります。オプション。
収容 java.lang.String
オブジェクトも永続世代に保存されます。 java.lang.String
クラスは文字列のプールを維持します。インターンメソッドが呼び出されると、メソッドはプールをチェックして、同等の文字列が存在するかどうかを確認します。その場合は、internメソッドによって返されます。そうでない場合、文字列はプールに追加されます。より正確に言えば、java.lang.String.intern
メソッドは文字列を返します 正規表現 ;結果は、その文字列がリテラルとして表示された場合に返される同じクラスインスタンスへの参照です。アプリケーションが多数の文字列をインターンする場合は、永続世代のサイズを増やす必要がある場合があります。
注:jmap -permgen
を使用できます内部化された文字列インスタンスに関する情報を含む、永続的な生成に関連する統計を出力するコマンド。
このエラーは、アプリケーション(またはそのアプリケーションで使用されるAPI)がヒープサイズよりも大きい配列を割り当てようとしたことを示しています。たとえば、アプリケーションが512MBの配列を割り当てようとしたが、最大ヒープサイズが256MBの場合、このエラーメッセージとともにOOMがスローされます。ほとんどの場合、問題は構成の問題か、アプリケーションが大規模なアレイを割り当てようとしたときに発生するバグのいずれかです。
このメッセージはOOMのようです。ただし、ネイティブヒープからの割り当てが失敗し、ネイティブヒープが使い果たされる可能性がある場合、HotSpotVMはこの明らかな例外をスローします。メッセージには、失敗した要求のサイズ(バイト単位)とメモリ要求の理由が含まれています。ほとんどの場合、は割り当ての失敗を報告しているソースモジュールの名前です。
このタイプのOOMがスローされた場合、問題をさらに診断するために、オペレーティングシステムのトラブルシューティングユーティリティを使用する必要がある場合があります。場合によっては、問題がアプリケーションに関連していないこともあります。たとえば、次の場合にこのエラーが表示されることがあります。
オペレーティングシステムが不十分なスワップスペースで構成されています。
システム上の別のプロセスは、使用可能なすべてのメモリリソースを消費しています。
ネイティブリークが原因でアプリケーションが失敗した可能性もあります(たとえば、アプリケーションまたはライブラリコードの一部がメモリを継続的に割り当てているが、オペレーティングシステムへの解放に失敗した場合)。
このエラーメッセージが表示され、スタックトレースの最上位フレームがネイティブメソッドである場合、そのネイティブメソッドで割り当ての失敗が発生しています。このメッセージと前のメッセージの違いは、Javaメモリ割り当ての失敗がJavaVMコードではなくJNIまたはネイティブメソッドで検出されたことです。
このタイプのOOMがスローされた場合、問題をさらに診断するためにオペレーティングシステムのユーティリティを使用する必要がある場合があります。
場合によっては、ネイティブヒープからの割り当てが失敗した直後にアプリケーションがクラッシュすることがあります。これは、メモリ割り当て関数によって返されるエラーをチェックしないネイティブコードを実行している場合に発生します。
たとえば、malloc
システムコールはNULL
を返します使用可能なメモリがない場合。 malloc
から戻った場合がチェックされていない場合、無効なメモリ位置にアクセスしようとするとアプリケーションがクラッシュする可能性があります。状況によっては、このタイプの問題を見つけるのが難しい場合があります。
モジュール間の関係を視覚化するためにプログラマーが使用するツール
場合によっては、致命的なエラーログまたはクラッシュダンプからの情報で十分です。クラッシュの原因が一部のメモリ割り当てでのエラー処理の欠如であると判断された場合は、その割り当ての失敗の理由を突き止める必要があります。他のネイティブヒープの問題と同様に、システムが不十分なスワップスペースで構成されている、別のプロセスが使用可能なすべてのメモリリソースを消費しているなどの可能性があります。
ほとんどの場合、メモリリークを診断するには、問題のアプリケーションに関する非常に詳細な知識が必要です。警告:プロセスは長く、反復する可能性があります。
メモリリークを追跡するための戦略は比較的簡単です。
症状を特定する
詳細なガベージコレクションを有効にする
プロファイリングを有効にする
トレースを分析する
説明したように、多くの場合、Javaプロセスは最終的にOOMランタイム例外をスローします。これは、メモリリソースが使い果たされたことを明確に示します。この場合、通常のメモリの枯渇とリークを区別する必要があります。 OOMのメッセージを分析し、上記の議論に基づいて犯人を見つけようとします。
多くの場合、Javaアプリケーションがランタイムヒープが提供するよりも多くのストレージを要求する場合、それは不十分な設計が原因である可能性があります。たとえば、アプリケーションがイメージの複数のコピーを作成したり、ファイルを配列にロードしたりする場合、イメージまたはファイルが非常に大きいと、ストレージが不足します。これは通常のリソースの枯渇です。アプリケーションは設計どおりに機能しています(ただし、この設計は明らかに骨の折れるものです)。
ただし、アプリケーションが同じ種類のデータを処理しているときにメモリ使用率を着実に増加させると、メモリリークが発生する可能性があります。
実際にメモリリークがあると断言する最も簡単な方法の1つは、詳細なガベージコレクションを有効にすることです。通常、メモリ制約の問題は、verbosegc
のパターンを調べることで特定できます。出力。
具体的には、 -verbosegc
引数を使用すると、ガベージコレクション(GC)プロセスが開始されるたびにトレースを生成できます。つまり、メモリがガベージコレクションされると、要約レポートが標準エラーで出力され、メモリがどのように管理されているかがわかります。
–verbosegc
で生成される典型的な出力を次に示します。オプション:
このGCトレースファイルの各ブロック(またはスタンザ)には、昇順で番号が付けられています。このトレースを理解するには、連続するAllocation Failureスタンザを調べて、合計メモリ(ここでは、19725304)が増加している間に、解放されたメモリ(バイトとパーセンテージ)が時間とともに減少することを確認する必要があります。これらは、メモリ枯渇の典型的な兆候です。
JVMが異なれば、ヒープアクティビティを反映するトレースファイルを生成するさまざまな方法が提供されます。これには通常、オブジェクトのタイプとサイズに関する詳細情報が含まれます。これは ヒープのプロファイリング 。
この投稿では、JavaVisualVMによって生成されたトレースに焦点を当てています。トレースは、さまざまなJavaメモリリーク検出ツールで生成できるため、さまざまな形式で提供できますが、その背後にある考え方は常に同じです。ヒープ内に存在してはならないオブジェクトのブロックを見つけ、これらのオブジェクトが蓄積するかどうかを判断しますリリースする代わりに。特に興味深いのは、Javaアプリケーションで特定のイベントがトリガーされるたびに割り当てられることがわかっている一時オブジェクトです。少量でしか存在しないはずのオブジェクトインスタンスが多数存在する場合は、通常、アプリケーションのバグを示しています。
最後に、メモリリークを解決するには、コードを徹底的に確認する必要があります。オブジェクトのリークの種類を知ることは非常に役立ち、デバッグを大幅にスピードアップできます。
メモリリークの問題があるアプリケーションの分析を開始する前に、まず、JVMでガベージコレクションがどのように機能するかを見てみましょう。
JVMは、と呼ばれるガベージコレクタの形式を使用します トレースコレクター 、これは基本的に、周囲の世界を一時停止し、すべてのルートオブジェクト(実行中のスレッドによって直接参照されるオブジェクト)をマークし、それらの参照に従って、途中で表示される各オブジェクトをマークすることによって動作します。
Javaはと呼ばれるものを実装します 世代別 世代別仮説の仮定に基づくガベージコレクター。 作成されたオブジェクトの大部分はすぐに破棄されます 、および すぐに収集されないオブジェクトは、しばらくの間存在する可能性があります 。
この仮定に基づいて、 Javaはオブジェクトを複数の世代に分割します 。視覚的な解釈は次のとおりです。
若い世代 -ここからオブジェクトが始まります。 2つのサブ世代があります。
不動産鑑定における回帰分析
エデンスペース -オブジェクトはここから始まります。ほとんどのオブジェクトは、エデンスペースで作成および破棄されます。ここで、GCは マイナーGC 、最適化されたガベージコレクションです。マイナーGCが実行されると、まだ必要なオブジェクトへの参照は、サバイバースペースの1つ(S0またはS1)に移行されます。
サバイバースペース(S0およびS1) -エデンを生き残ったオブジェクトはここに行き着きます。これらは2つあり、常に1つだけが使用されています(深刻なメモリリークがない限り)。 1つはとして指定されます 空の 、および他の 住む 、GCサイクルごとに交互に。
テニュア世代 -旧世代(図2の古いスペース)とも呼ばれるこのスペースには、寿命の長い古いオブジェクトが保持されます(十分に長く存続する場合は、サバイバースペースから移動されます)。このスペースがいっぱいになると、GCは フルGC 、パフォーマンスの面でより多くの費用がかかります。このスペースが際限なく大きくなると、JVMはOutOfMemoryError - Java heap space
をスローします。
パーマネントジェネレーション -テニュア世代と密接に関連する第3世代である永続世代は、Java言語レベルで同等性を持たないオブジェクトを記述するために仮想マシンが必要とするデータを保持するため、特別です。たとえば、クラスとメソッドを記述するオブジェクトは、永続世代に格納されます。
Javaは、世代ごとに異なるガベージコレクションメソッドを適用できるほど賢いです。若い世代は、 トレース、コレクターのコピー と呼ばれる 並列の新しいコレクター 。このコレクターは世界を止めますが、若い世代は一般的に小さいので、一時停止は短いです。
JVM世代とそれらがどのように機能するかについての詳細は、次のWebサイトをご覧ください。 JavaHotSpot仮想マシンでのメモリ管理 ドキュメンテーション。
メモリリークを見つけて排除するには、適切なメモリリークツールが必要です。を使用してそのようなリークを検出して除去する時が来ました Java VisualVM 。
VisualVMは、Javaテクノロジベースのアプリケーションの実行中に詳細情報を表示するためのビジュアルインターフェイスを提供するツールです。
VisualVMを使用すると、ローカルアプリケーションおよびリモートホストで実行されているアプリケーションに関連するデータを表示できます。また、JVMソフトウェアインスタンスに関するデータをキャプチャして、ローカルシステムに保存することもできます。
Java VisualVMのすべての機能を利用するには、Java Platform、Standard Edition(Java SE)バージョン6以降を実行する必要があります。
関連: すでにJava8にアップグレードする必要がある理由本番環境では、コードが実行される実際のマシンにアクセスするのが難しいことがよくあります。幸い、Javaアプリケーションをリモートでプロファイリングできます。
まず、ターゲットマシンへのJVMアクセスを許可する必要があります。これを行うには、というファイルを作成します jstatd.all.policy 次の内容で:
grant codebase 'file:${java.home}/../lib/tools.jar' { permission java.security.AllPermission; };
ファイルが作成されたら、を使用してターゲットVMへのリモート接続を有効にする必要があります。 jstatd-仮想マシンjstatデーモン 次のようなツール:
jstatd -p -J-Djava.security.policy=
例えば:
jstatd -p 1234 -J-Djava.security.policy=D:jstatd.all.policy
とともに jstatd ターゲットVMで開始すると、ターゲットマシンに接続して、メモリリークの問題があるアプリケーションをリモートでプロファイリングできます。
クライアントマシンで、プロンプトを開き、jvisualvm
と入力します。 VisualVMツールを開きます。
次に、VisualVMにリモートホストを追加する必要があります。ターゲットJVMが有効になっているため、J2SE 6以降を搭載した別のマシンからのリモート接続が可能になっているため、Java VisualVMツールを起動して、リモートホストに接続します。リモートホストとの接続が成功すると、次のように、ターゲットJVMで実行されているJavaアプリケーションが表示されます。
アプリケーションでメモリプロファイラーを実行するには、サイドパネルでその名前をダブルクリックするだけです。
これでメモリアナライザの設定が完了したので、メモリリークの問題があるアプリケーションを調査してみましょう。これを呼び出します。 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つしかないため、コードは無限ループでも正常に実行されます。
(興味のある方は、 ここに (意図的に)リークを生成するいくつかの代替手段です。)
Java VisualVMを使用すると、Javaヒープをメモリ監視し、その動作がメモリリークを示しているかどうかを識別できます。
これは、初期化直後のMemLeakのJavaヒープアナライザーのグラフィック表現です(さまざまな説明を思い出してください) 世代 ):
わずか30秒後、旧世代はほぼ満杯になります。これは、フルGCを使用しても、旧世代が増え続けていることを示しており、メモリリークの明らかな兆候です。
このリークの原因を検出する1つの方法を次の画像に示します( クリックしてズーム )、JavaVisualVMを使用して生成されます。 ヒープダンプ 。ここでは、 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ヒープダンプ 。
ギリシャのデフォルトはユーロにどのように影響しますか
メモリリークとは、メモリが不要になったにもかかわらず「使用中」とマークされた場合であり、メモリを作成したプロセスによって解放されることはありません。
それは問題のリークに依存します:いくつかは他のものより見つけて修正するのが簡単です。結局、不要になったオブジェクトへの参照を保持しないことになります。これは独自のコードにある場合もありますが、コードが使用するライブラリにある場合もあります。
アプリケーションが同じ種類のデータを処理しているときにメモリ使用率を着実に増加させると、メモリリークが発生する可能性があります。トレースを分析することで、リークソースを絞り込むことができます。少量でのみ存在するはずの多くのオブジェクトインスタンスの存在は、通常、メモリリークを示します。
ヒープサイズは、JVMが割り当てに使用できるメモリの量を決定します。新しいオブジェクトを作成するたびに、そのオブジェクトはヒープに連続して保存されます。
JVMは、データのグループごとに異なるガベージコレクション方法を使用します。データは、破棄されるまでの時間によって分類されます。短期間のデータの場合、すべてを一時停止し、トレースを使用して、コレクターをコピーして、参照されなくなったメモリを解放します。
Javaでのメモリリークの修正には、症状の観察、詳細なGCとプロファイリングの使用、メモリトレースの分析が含まれ、その後、リークに関係するオブジェクトを利用するコードを徹底的に確認します。