1.JVMメモリ領域とオブジェクトメモリレイアウト

1.ランタイムデータ領域

JVMのメモリ領域図は次のとおりです。

JVMメモリエリアマップ

1.1、プログラムカウンタ

プログラムカウンタレジスタは小さなメモリスペースであり、現在のスレッドによって実行されるバイトコードの行番号インジケータと見なすことができます。Java仮想マシンの概念モデル[1]では、バイトコードインタープリターが機能すると、このカウンターの値を変更することにより、実行する次のバイトコード命令を選択します。これは、プログラム制御フロー、分岐、およびループのインジケーターです。ジャンプ、例外処理、スレッド回復などの基本機能はすべて、完了するためにこのカウンターに依存する必要があります。Java仮想マシンのマルチスレッド化は、スレッドの切り替えとプロセッサの実行時間の割り当てによって実現されるため、特定の時点で、プロセッサ(マルチコアプロセッサの場合はコア)は1つの命令のみを実行します。スレッド。したがって、スレッドを切り替えた後に正しい実行位置に復元するには、各スレッドに独立したプログラムカウンタが必要です。スレッド間のカウンタは相互に影響を与えず、独立して格納されます。このタイプのメモリ領域を「スレッドプライベート」メモリ。スレッドがJavaメソッドを実行している場合、このカウンターは実行されている仮想マシンのバイトコード命令のアドレスを記録します。スレッドがネイティブメソッドを実行している場合、カウンター値は未定義である必要があります。このメモリ領域は、「Java仮想マシン仕様」でOutOfMemoryError条件を指定していない唯一の領域です。

1.2、仮想マシンスタック

プログラムカウンタと同様に、Java仮想マシンスタック(Java仮想マシンスタック)もスレッドプライベートであり、そのライフサイクルはスレッドのライフサイクルと同じです。仮想マシンスタックは、Javaメソッド実行のスレッドメモリモデルを記述します。各メソッドが実行されると、Java仮想マシンは、ローカル変数テーブル、オペランドスタック、動的接続、およびメソッドを格納するスタックフレームを同期的に作成します。エクスポートおよびその他の情報。各メソッドが呼び出されてから実行が完了するまでのプロセスは、仮想マシンスタックでのプッシュからポップまでのスタックフレームのプロセスに対応します。

ローカル変数テーブル内のこれらのデータ型のストレージスペースは、ローカル変数スロット(Slot)で表されます。ここで、64ビットのlongデータとdoubleデータは2つの変数スロットを占有し、残りのデータ型は1つのみを占有します。ローカル変数テーブルに必要なメモリスペースはコンパイル時に割り当てられます。メソッドを入力すると、このメソッドがスタックフレームに割り当てる必要のあるローカル変数スペースの量が完全に決定され、ローカル変数テーブルのサイズは変更されません。メソッドの実行。ここでの「サイズ」とは、可変スロットの数、仮想マシンが可変スロットを実現するために実際に使用するメモリスペースの量(たとえば、32ビットまたは64ビット以上を占める可変スロットによる)を指すことに注意してください。 、これは、特定の仮想マシンの実装によって完全に決定されます。「Java仮想マシン仕様」では、このメモリ領域に2種類の異常状態が指定されています。スレッドによって要求されたスタックの深さが仮想マシンによって許可された深さよりも大きい場合、StackOverflowError例外がスローされます。Javaの場合仮想マシンのスタック容量は動的に拡張できます。スタックが拡張されると、十分なメモリが利用できない場合にOutOfMemoryErrorがスローされます。

1.3、ローカルメソッドスタック

ネイティブメソッドスタック(ネイティブメソッドスタック)と仮想マシンスタックは非常によく似た役割を果たします。違いは、仮想マシンスタックが仮想マシンにJavaメソッド(つまりバイトコード)を実行するのに対し、ネイティブメソッドスタックは仮想マシンにサービスを提供することです。マシンによって使用されるネイティブメソッドサービス。「Java仮想マシン仕様」には、ローカルメソッドスタック内のメソッドの言語、使用法、データ構造に関する必須の規定がないため、特定の仮想マシンは必要に応じて自由に実装でき、一部のJava仮想マシン( Hot-Spot(仮想マシン)など)は、ローカルメソッドスタックと仮想マシンスタックを直接1つに結合します。仮想マシンスタックと同様に、ローカルメソッドスタックも、スタックの深さがオーバーフローした場合、またはスタックの拡張が失敗した場合に、それぞれStackOverflowErrorおよびOutOfMemoryError例外をスローします。

1.4、Javaヒープ

Javaアプリケーションの場合、Javaヒープは、仮想マシンによって管理される最大のメモリです。Javaヒープは、すべてのスレッドで共有されるメモリ領域であり、仮想マシンの起動時に作成されます。このメモリ領域の唯一の目的は、オブジェクトインスタンスを格納することであり、Javaの世界の「ほぼ」すべてのオブジェクトインスタンスがここにメモリを割り当てます。「Java仮想マシン仕様」でのJavaヒープの説明は次のとおりです。「すべてのオブジェクトインスタンスと配列はヒープに割り当てる必要があります」。ここでの作成者は、実装の観点から、Java言語を使用することを意味します。ジャストインタイムコンパイルテクノロジ、特にますます強力になるエスケープ分析テクノロジの進歩により、現在だけを考慮しても、値タイプのサポートが将来登場する可能性があるという兆候がいくつか見られます。割り当ての最適化方法スタック上でのスカラー置換により、いくつかの微妙な変更が静かに行われるため、Javaオブジェクトインスタンスがヒープに割り当てられ、徐々に絶対値が低くなります。Javaヒープは、ガベージコレクターによって管理されるメモリ領域であるため、一部の資料では「GCヒープ」とも呼ばれます(ガベージコレクションヒープは、幸い、中国では「ガベージヒープ」に変換されません)。記憶を取り戻すという観点から、現代​​のガベージコレクターのほとんどは世代別コレクション理論に基づいて設計されているため、「新世代」、「旧世代」、「永続世代」、「エデンスペース」、「サバイバーから」がよくあります。 Javaヒープ。「スペース」や「サバイバースペースへ」などの名詞は、この本の後続の章で繰り返し表示されます。ここでは、これらの区分がガベージコレクターの一般的な機能またはデザインスタイルの一部にすぎないことを最初に説明します。特定のJava仮想マシンによって実装される固有のメモリレイアウトでも、「Java仮想マシン仕様」でのJavaヒープのさらに詳細な分割でもありません。多くの資料では、「Java仮想マシンのヒープメモリは、若い世代、古い世代、永続的な世代、エデン、サバイバーに分けられます...」などとよく言われます。10年前(G1コレクターの出現を境界として)、業界の絶対的な主流のHotSpot仮想マシンとして、その内部ガベージコレクターはすべて「クラシック世代」に基づいて設計されており、新世代と旧世代のコレクターが一致する必要があります。この文脈では、上記のステートメントはあまり曖昧ではありません。しかし、今日、ガベージコレクターの技術は10年前と同じではなく、HotSpotも公開しています 世代別設計を採用していない新しいガベージコレクターがあり、上記の定式化に従って議論すべき点がたくさんあります。メモリ割り当ての観点から、すべてのスレッドで共有されるJavaヒープを複数のスレッド専用割り当てバッファー(スレッドローカル割り当てバッファー、TLAB)に分割して、オブジェクト割り当ての効率を向上させることができます。ただし、どの角度から見ても、分割に関係なく、Javaヒープ内のストレージコンテンツの共通性は変わりません。どの領域に関係なく、ストレージはオブジェクトのインスタンスにしかなれません。 Javaヒープは、より良いリサイクルのためだけのものです。メモリ、またはメモリをより速く割り当てます。この章では、メモリ領域の役割についてのみ説明します。Javaヒープ内の上記の領域の割り当てと回復の詳細については、次の章で説明します。「Java仮想マシン仕様」によると、Javaヒープは物理的に不連続なメモリ空間にある可能性がありますが、論理的には連続していると見なす必要があります。これは、ファイルを格納するためにディスク領域を使用する場合と同じです。継続的に保管されます。ただし、大きなオブジェクト(通常は配列オブジェクト)の場合、ほとんどの仮想マシンの実装では、単純な実装と高いストレージ効率のために、連続したメモリスペースが必要になる可能性があります。Javaヒープは、固定サイズまたは拡張可能として実装できますが、現在の主流のJava仮想マシンは、スケーラビリティー(パラメーター-Xmxおよび-Xmsで設定)に従って実装されます。インスタンスの割り当てを完了するためのメモリがJavaヒープになく、ヒープを拡張できなくなった場合、Java仮想マシンはOutOfMemoryError例外をスローします。「Java仮想マシン仕様」によると、Javaヒープは物理的に不連続なメモリ空間にある可能性がありますが、論理的には連続していると見なす必要があります。これは、ファイルを格納するためにディスク領域を使用する場合と同じです。継続的に保管されます。ただし、大きなオブジェクト(通常は配列オブジェクト)の場合、ほとんどの仮想マシンの実装では、単純な実装と高いストレージ効率のために、連続したメモリスペースが必要になる可能性があります。Javaヒープは、固定サイズまたは拡張可能として実装できますが、現在の主流のJava仮想マシンは、スケーラビリティー(パラメーター-Xmxおよび-Xmsで設定)に従って実装されます。インスタンスの割り当てを完了するためのメモリがJavaヒープになく、ヒープを拡張できなくなった場合、Java仮想マシンはOutOfMemoryError例外をスローします。「Java仮想マシン仕様」によると、Javaヒープは物理的に不連続なメモリ空間にある可能性がありますが、論理的には連続していると見なす必要があります。これは、ファイルを格納するためにディスク領域を使用する場合と同じです。継続的に保管されます。ただし、大きなオブジェクト(通常は配列オブジェクト)の場合、ほとんどの仮想マシンの実装では、単純な実装と高いストレージ効率のために、連続したメモリスペースが必要になる可能性があります。Javaヒープは、固定サイズまたは拡張可能として実装できますが、現在の主流のJava仮想マシンは、スケーラビリティー(パラメーター-Xmxおよび-Xmsで設定)に従って実装されます。インスタンスの割り当てを完了するためのメモリがJavaヒープになく、ヒープを拡張できなくなった場合、Java仮想マシンはOutOfMemoryError例外をスローします。

1.5、メソッド領域

メソッド領域は、Javaヒープと同様に、各スレッドによって共有されるメモリ領域であり、ロードされたジャストインタイムコンパイラによってコンパイルされた型情報、定数、静的変数、コードキャッシュなどのデータを格納するために使用されます。仮想マシンによって。「Java仮想マシン仕様」では、メソッド領域をヒープの論理部分として説明していますが、Javaヒープと区別するために「非ヒープ」と呼ばれるエイリアスがあります。メソッド領域について言えば、「永続生成」の概念について言及する必要があります。特にJDK 8より前は、多くのJavaプログラマーがHotSpot仮想マシンでのプログラムの開発とデプロイに慣れており、多くの人がメソッド領域を「」と呼ぶことを好みます。パーマネントジェネレーション」(パーマネントジェネレーション)、または2つを混同します。本質的に、この2つは同等ではありません。これは、当時のHotSpot仮想マシン設計チームが、コレクターの世代別設計をメソッド領域に拡張するか、永続的な生成を使用してメソッド領域を実装することを選択したため、HotSpotがゴミになるためです。 Javaヒープのようにメモリのこの部分を管理し、メソッド領域専用のメモリ管理コードを作成する手間を省きます。ただし、BEA JRockit、IBM J9などの他の仮想マシンの実装では、永続的な生成の概念はありません。原則として、メソッド領域の実装方法は、「Java仮想マシン仕様」の対象外であり、統一性を必要としない仮想マシンの実装詳細に属します。しかし、今振り返ってみると、メソッド領域を実装するために永続世代を使用するという決定は良い考えではありませんでした。この設計により、Javaアプリケーションでメモリオーバーフローの問題が発生する可能性が高くなりました(永続世代の上限は-XX:MaxPermSizeです。設定にはデフォルトのサイズもあり、J9とJRockitがプロセスで使用可能なメモリの上限(32ビットシステムの4GBの制限など)に触れない限り、問題はありません)。メソッドはほとんどありません(String ::など)。intern())は、永続的な生成により、異なる仮想マシンで異なるパフォーマンスを引き起こします。オラクルがBEAを買収し、JRockitの所有権を取得した後、Java Mission Control管理ツールなどのJRockitの優れた機能をHotSpot仮想マシンに移行する準備が整いましたが、メソッド領域の違いにより多くの問題に直面しました。二つ。HotSpotの将来の開発を考慮して、JDK 6では、HotSpot開発チームは永続的な生成を放棄し、メソッド領域を実装するためにネイティブメモリを使用する計画に徐々に変更しました。JDK7のHotSpotでは、元の永続的な生成文字列定数プール、静的変数などが削除され、JDK 8では、永続生成の概念が最終的に完全に廃止されました。代わりに、JRockitやJ9などのローカルメモリに実装されたメタスペース(Metaspace)が使用され、JDKが使用されました。 7の永続世代の残りのコンテンツ(主にタイプ情報)はすべてメタスペースに移動されます。「Java仮想マシン仕様」では、メソッド領域の制限が大幅に緩和されています。Javaヒープと同じだけでなく、連続したメモリを必要とせず、固定サイズまたは拡張可能を選択できます。ガベージコレクションを実装しないこともできます。 。比較的言えば、この領域ではガベージコレクションは確かに比較的まれですが、データが永続世代の名前として「永続的」としてメソッド領域に入力されるわけではありません。この領域でのメモリ回復の目標は、主に定数プールの回復と型のアンロードです。一般的に、この領域の回復効果はより困難で満足のいくものです。特に型のアンロードの場合、条件は非常に厳しいです。 、しかし地域のこの部分の回復は時々必要ですそれは確かに必要です。以前のSun社のバグリストで発生したいくつかの重大なバグは、この領域を完全に再利用しなかったHotSpot仮想マシンの低バージョンが原因であり、メモリリークが発生していました。「Java仮想マシン仕様」では、メソッド領域の制限が大幅に緩和されています。Javaヒープと同じだけでなく、連続したメモリを必要とせず、固定サイズまたは拡張可能を選択できます。ガベージコレクションを実装しないこともできます。 。比較的言えば、この領域ではガベージコレクションは確かに比較的まれですが、データが永続世代の名前として「永続的」としてメソッド領域に入力されるわけではありません。この領域でのメモリ回復の目標は、主に定数プールの回復と型のアンロードです。一般的に、この領域の回復効果はより困難で満足のいくものです。特に型のアンロードの場合、条件は非常に厳しいです。 、しかし地域のこの部分の回復は時々必要ですそれは確かに必要です。以前のSun社のバグリストで発生したいくつかの重大なバグは、この領域を完全に再利用しなかったHotSpot仮想マシンの低バージョンが原因であり、メモリリークが発生していました。「Java仮想マシン仕様」では、メソッド領域の制限が大幅に緩和されています。Javaヒープと同じだけでなく、連続したメモリを必要とせず、固定サイズまたは拡張可能を選択できます。ガベージコレクションを実装しないこともできます。 。比較的言えば、この領域ではガベージコレクションは確かに比較的まれですが、データが永続世代の名前として「永続的」としてメソッド領域に入力されるわけではありません。この領域でのメモリ回復の目標は、主に定数プールの回復と型のアンロードです。一般的に、この領域の回復効果はより困難で満足のいくものです。特に型のアンロードの場合、条件は非常に厳しいです。 、しかし地域のこの部分の回復は時々必要ですそれは確かに必要です。以前のSun社のバグリストで発生したいくつかの重大なバグは、この領域を完全に再利用しなかったHotSpot仮想マシンの低バージョンが原因であり、メモリリークが発生していました。

1.6、ランタイム定数プール

ランタイム定数プールはメソッド領域の一部です。クラスファイル内のクラスバージョン、フィールド、メソッド、およびインターフェイスの説明情報に加えて、コンパイル時に生成されたさまざまなリテラルおよびシンボリック参照を格納するために使用される定数ビリヤード台(定数ビリヤード台)もあります。この部分コンテンツは、クラスがロードされた後、メソッド領域のランタイム定数プールに格納されます。Java仮想マシンには、クラスファイルの各部分(当然、定数プールを含む)の形式に関する厳しい規制があります。たとえば、各バイトの格納に使用されるデータの種類は、認識される前に仕様の要件を満たす必要があります。 、仮想マシンによってロードおよび実行されますが、ランタイム定数プールの場合、「Java仮想マシン仕様」では詳細な要件はありません。さまざまなプロバイダーによって実装された仮想マシンは、独自のニーズに応じてこのメモリ領域を実装できますが、一般的に言えば、クラスファイルの保存に加えて、説明されているシンボリック参照に加えて、シンボリック参照から変換された直接参照もランタイム定数プールに保存されます[1]。クラスファイル定数プールと比較したランタイム定数プールのもう1つの重要な機能は、動的であるということです。Java言語では、コンパイル時にのみ定数を生成する必要はありません。つまり、定数プールのコンテンツが事前設定されていません。クラスファイルを入力できます。メソッド領域は実行時定数プールであり、実行時に新しい定数をプールに入れることもできます。この機能は、開発者がStringクラスのintern()メソッドでより頻繁に使用します。ランタイム定数プールはメソッド領域の一部であるため、当然、メソッド領域のメモリによって制限されます。定数プールがメモリに適用できなくなると、OutOfMemoryError例外がスローされます。

1.7、ダイレクトメモリ

ダイレクトメモリ(ダイレクトメモリ)は、仮想マシンランタイムのデータ領域の一部ではなく、「Java仮想マシン仕様」で定義されているメモリ領域でもありません。ただし、メモリのこの部分も頻繁に使用され、OutOfMemoryErrorが表示される可能性もあるため、ここで説明します。JDK 1.4では、NIO(New Input / Output)クラスが新たに追加され、チャネルおよびバッファベースのI / Oメソッドが導入されました。ネイティブ関数ライブラリを使用してオフヒープメモリを直接割り当て、操作方法を指定できます。このメモリへの参照として、Javaヒープに格納されているDirectByteBufferオブジェクトを使用します。これにより、Javaヒープとネイティブヒープの間でデータをやり取りすることが回避されるため、一部のシナリオでパフォーマンスが大幅に向上する可能性があります。明らかに、ネイティブダイレクトメモリの割り当てはJavaヒープのサイズによって制限されませんが、メモリであるため、ネイティブメモリ全体(物理メモリ、SWAPパーティション、またはページングファイルを含む)のサイズによって確実に影響を受けます。およびプロセッサのアドレス空間仮想マシンのパラメータを構成する場合、一般的なサーバー管理者は実際のメモリに応じて-Xmxおよびその他のパラメータ情報を設定しますが、直接メモリを無視することが多く、各メモリ領域の合計が物理メモリの制限(物理メモリとオペレーティングシステムレベルの制限)、動的拡張中にOutOfMemoryError例外が発生します。

 

2.HotSpot仮想マシンオブジェクトの探索

2.1オブジェクトの作成

Java仮想マシンがバイトコードの新しい命令を検出すると、最初に、この命令のパラメータが定数プール内のクラスのシンボル参照を見つけることができるかどうかを確認し、シンボル参照によって表されるクラスがロード、解決されているかどうかを確認します。および初期化。そうでない場合は、最初に対応するクラスのロードプロセスを実行する必要があります。クラスの読み込みチェックに合格すると、仮想マシンは新しいオブジェクトにメモリを割り当てます。オブジェクトに必要なメモリのサイズは、クラスがロードされた後に完全に決定できます。

メモリ割り当て方法には、「ポインタの衝突」と「フリーリスト」の方法があります。

Javaメモリが絶対的に規則的である場合、「ポインタ衝突」を使用して割り当てることができます。使用済みメモリはすべて片側に配置され、空きメモリは反対側に配置され、ポインタは境界点のインジケータとして中央に配置されます。 。、割り当てられたメモリは、オブジェクトのサイズに等しい距離だけポインタを空き領域に移動するためのものです。この割り当て方法は「BumpThePointer」と呼ばれます。この割り当て方法は、マークアンドソートベースのGCアルゴリズムを使用する場合に使用できます。

Javaヒープ内のメモリが規則的でなく、使用済みメモリと空きメモリが相互にインターレースされている場合、ポインタを単純に衝突させる方法はありません。仮想マシンは、記録されているメモリブロックのリストを維持する必要があります。 、割り当て中にオブジェクトインスタンスに割り当てるのに十分な大きさのスペースをリストから見つけ、リストのレコードを更新します。この割り当て方法は「フリーリスト」と呼ばれます。この割り当て方法は、マークアンドスイープベースのGCアルゴリズムを使用する場合に使用できます。

同時割り当てを処理する場合、2つの解決策があります。1つはメモリスペースを割り当てるアクションを同期することです。実際、仮想マシンはCASを使用して失敗再試行し、更新操作のアトミック性を確保します。もう1つはメモリのアクションです。割り当ては、スレッドの分割に応じて異なるスペースで実行されます。つまり、各スレッドは、ローカルスレッド割り当てバッファー(Thread Local Allocation Buffer、TLAB)と呼ばれるJavaヒープ内の小さなメモリを事前に割り当てます。メモリ、どのスレッドのローカルバッファに割り当てられますか。ローカルバッファが使い果たされた場合にのみ、新しいバッファ領域が割り当てられたときに同期ロックが必要になります。仮想マシンがTLABを使用するかどうかは、-XX:+/- UseTLABパラメーターで設定できます。

2.2オブジェクトメモリのレイアウト

HotSpot仮想マシンのオブジェクトのメモリレイアウトは、オブジェクトヘッダー(ヘッダー)、インスタンスデータ(インスタンスデータ)、および配置パディング(パディング)の3つに分けることができます。

2.2.1、オブジェクトヘッダー

       写真から、オブジェクトヘッダーはマークワードとクラスポインタの2つの部分に分かれていることがわかります。

Mark Wordは、オブジェクトのhashCode、GC情報、およびロック情報を格納します。ClassPointerは、クラスオブジェクトの情報へのポインタを格納します。32ビットJVMでは、オブジェクトヘッダーのサイズは8バイト、64ビットJVMは16バイトです。2種類のマークワードとクラスポインターは、それぞれスペースの半分を占めます。64ビットJVMには圧縮ポインターオプション-XX:+ UseCompressedOopsがあり、これはデフォルトで有効になっています。開いた後、クラスポインタ部分は4バイトに圧縮され、オブジェクトヘッダーサイズは12バイトに縮小されます。

次の図は、32ビットJVMでのオブジェクトヘッダーのメモリ分布を示しています。これは理解しやすいものです。

2.2.2、インスタンスデータと配置パディング

       ユーザープログラムで定義された可変メモリ割り当ては、仮想マシン割り当て戦略パラメーター(-XX:FieldsAllocationStyleパラメーター)およびJavaソースコードでフィールドが定義された順序の影響を受けます。HotSpot仮想マシンのデフォルトの割り当て順序は、long / doubles、ints、shorts / chars、bytes / booleans、oops(通常のオブジェクトポインター、OOP)です。上記のデフォルトの割り当て戦略からわかるように、同じ幅のフィールドは常に割り当て済み一緒に保存されます。この前提条件を満たしている場合、親クラスで定義された変数が子クラスの前に表示されます。HotSpot仮想マシンの+ XX:CompactFieldsパラメーター値がtrue(デフォルトはtrue)の場合、サブクラスのより狭い変数を親クラス変数のギャップに挿入して、スペースを少し節約することもできます。HotSpot仮想マシンの自動メモリ管理システムでは、オブジェクトの開始アドレスが8バイトの倍数である必要があるため、つまり、オブジェクトのサイズは8バイトの倍数である必要があります。インスタンスデータ部分が整列されていない次に、整列する必要があります。

2.3。オブジェクトアクセス場所

主流のアクセス方法には、主にハンドルと直接ポインタの使用が含まれます。

1.ハンドルアクセスを使用する場合、メモリの一部がハンドルプールとしてJavaヒープに分割される場合があります。オブジェクトのハンドルアドレスは参照に格納され、ハンドルにはオブジェクトインスタンスデータの特定のアドレス情報が含まれます。タイプデータ。

2.直接ポインタアクセスを使用する場合、Javaヒープ内のオブジェクトのメモリレイアウトは、アクセスタイプデータに関する情報を配置する方法を考慮する必要があります。オブジェクトに格納される参照は、オブジェクトのアドレスです。オブジェクト自体、再度実行する必要はありません。間接アクセスのオーバーヘッド。

 

3.OutOfMemoryError例外

3.1、Javaヒープオーバーフロー

/**
* VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
* @author zzm
*/
public class HeapOOM {
    static class OOMObject {
    }
    public static void main(String[] args) {
        List<OOMObject> list = new ArrayList<OOMObject>();
        while (true) {
            list.add(new OOMObject());
        }
    }
}

テストコードは上記のとおりです。JavaヒープメモリのOutOfMemoryError例外は、実際のアプリケーションで最も一般的なメモリオーバーフロー例外です。Javaヒープメモリオーバーフローが発生すると、例外スタック情報「java.lang.OutOfMemoryError」がさらにプロンプ​​ト「Javaヒープスペース」の後に続きます。

このメモリ領域の異常を解決するために、従来の処理方法では、最初にメモリイメージ分析ツール(Eclipseメモリアナライザなど)を使用して、ダンプからのヒープダンプスナップショットを分析します。最初のステップは、OOMを引き起こすメモリ内のオブジェクトが必要かどうかを確認することです。つまり、メモリリーク(メモリリーク)またはメモリオーバーフロー(メモリオーバーフロー)があるかどうかを区別します。

メモリリークの場合は、ツールを使用して、リークされたオブジェクトのGCルートへの参照チェーンをさらに確認し、リークされたオブジェクトが通過する参照パスと関連付けられているGCルートを確認して、ガベージコレクターを作成できます。リークされたオブジェクトのタイプに応じて、それらを再利用することはできません。情報とそのGCルート参照チェーンへの情報は、通常、これらのオブジェクトが作成された場所をより正確に特定し、メモリリークの原因となったコードの特定の場所を見つけることができます。

メモリリークではない場合、つまりメモリ内のすべてのオブジェクトが有効である必要がある場合は、Java仮想マシンのヒープパラメータ(-Xmxおよび-Xms)設定を確認し、マシンのメモリと比較する必要があります。上向きの調整スペースがあるかどうかを確認します。次に、プログラム操作中のメモリ消費を最小限に抑えるために、ライフサイクルが長すぎる、保持状態時間が長すぎる、ストレージ構造の設計が不当であるなどのオブジェクトがないかどうかをコードから確認します。

3.2仮想マシンスタックとローカルメソッドスタックのオーバーフロー

仮想マシンスタックとローカルメソッドスタックに関して、「Java仮想マシン仕様」には2つの例外が記載されています。

1)スレッドによって要求されたスタックの深さが仮想マシンによって許可された最大の深さよりも大きい場合、StackOverflowError例外がスローされます。

2)仮想マシンのスタックメモリが動的拡張を許可している場合、拡張スタック容量が十分なメモリを適用できないと、OutOfMemoryError例外がスローされます。

/**
* 不停的递归导致栈溢出
* VM Args:-Xss128k
* @author zzm
*/
public class JavaVMStackSOF {
    private int stackLength = 1;
    public void stackLeak() {
        stackLength++;
        stackLeak();
    }
    public static void main(String[] args) throws Throwable {
        JavaVMStackSOF oom = new JavaVMStackSOF();
        try {
            oom.stackLeak();
        } catch (Throwable e) {
            System.out.println("stack length:" + oom.stackLength);
            throw e;
        }
    }
}

/**
* 不停创建线程导致栈溢出
* VM Args:-Xss2M (这时候不妨设大些,请在32位系统下运行)
* @author zzm
*/
public class JavaVMStackOOM {
    private void dontStop() {
        while (true) {
        }
    }
    public void stackLeakByThread() {
        while (true) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    dontStop();
                }
            });
            thread.start();
        }
    }
    public static void main(String[] args) throws Throwable {
        JavaVMStackOOM oom = new JavaVMStackOOM();
        oom.stackLeakByThread();
    }
}

StackOverflowError例外が発生すると、分析用の明確なエラースタックがあり、問題を見つけるのは比較的簡単です。HotSpot仮想マシンのデフォルトのパラメーターを使用する場合、スタックの深さはほとんどの場合です(各方法でスタックにプッシュされるフレームサイズは同じではないため、ほとんどの場合としか言えません)問題はありません1000〜2000に到達します。通常のメソッド呼び出し(末尾再帰用に最適化できない再帰呼び出しを含む)の場合、この深さで十分です。ただし、確立されたスレッドが多すぎるためにメモリオーバーフローが発生した場合、スレッド数を減らすことができないか、64ビット仮想マシンを置き換えることができない場合、より多くのスレッドと交換する唯一の方法は、最大ヒープを減らしてスタック容量を減らします。メモリオーバーフローを解決するために「メモリを削減する」この方法は、この分野の経験がなければ一般的に考えるのが困難です。読者は、32ビットシステム用のマルチスレッドアプリケーションを開発するときにこの点に注意する必要があります。これは、この問題が比較的隠されているためでもあります。JDK7以降、上記のプロンプトメッセージで「ネイティブスレッドを作成できません」と表示された後、仮想マシンは、理由が「メモリ不足またはプロセス/リソースの制限に達した可能性がある」ことを具体的に示しています。 "。

 

3.3メソッド領域と実行時定数プールのオーバーフロー

/**
* 使用GLIBC不断生成动态类型导致方法区溢出
* VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M
* @author zzm
*/
public class JavaMethodAreaOOM {
	public static void main(String[] args) {
		while (true) {
			Enhancer enhancer = new Enhancer();
			enhancer.setSuperclass(OOMObject.class);
			enhancer.setUseCache(false);
			enhancer.setCallback(new MethodInterceptor() {
				public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
					return proxy.invokeSuper(obj, args);
				}
			});
			enhancer.create();
		}
	}
	static class OOMObject {
	}
}

メソッド領域のオーバーフローも一般的なメモリオーバーフローの例外です。クラスがガベージコレクタによってリサイクルされる場合、達成される条件は比較的過酷です。頻繁な操作中に多数の動的クラスが生成されるアプリケーションシナリオでは、これらのクラスのリサイクルステータスに特別な注意を払う必要があります。JDK 8以降、永続世代は歴史の段階から完全に撤退し、Metaspaceがその代替として登場します。

HotSpotは、メタスペースの防御手段として、次のようないくつかのパラメーターを提供します。

-XX:MaxMetaspaceSize:最大メタスペースサイズを設定します。デフォルトは-1です。つまり、制限なし、またはローカルメモリサイズによってのみ制限されます。

-XX:MetaspaceSize:メタスペースの初期スペースサイズをバイト単位で指定します。この値に達すると、タイプのアンロードのためにガベージコレクションがトリガーされ、コレクターが値を調整します。大量のスペースが解放されると、この値を減らします。解放されるスペースが非常に少ない場合は、値が-XXを超えない場合は適切に増やします:MaxMetaspaceSize(設定されている場合)。

-XX:MinMetaspaceFreeRatio:この関数は、ガベージコレクション後の最小メタスペースの残り容量のパーセンテージを制御することです。これにより、メタスペースが不十分なためにガベージコレクションの頻度を減らすことができます。同様に、-XX:Max-MetaspaceFreeRatioがあります。これは、最大のメタスペースの残りの容量のパーセンテージを制御するために使用されます。

3.4ネイティブダイレクトメモリオーバーフロー

/**
* 通过Unsafe不断分配堆外内存最后导致本机内存溢出
* VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
* @author zzm
*/
public class DirectMemoryOOM {
	private static final int _1MB = 1024 * 1024;
	public static void main(String[] args) throws Exception {
		Field unsafeField = Unsafe.class.getDeclaredFields()[0];
		unsafeField.setAccessible(true);
		Unsafe unsafe = (Unsafe) unsafeField.get(null);
		while (true) {
			unsafe.allocateMemory(_1MB);
		}
	}
}

ダイレクトメモリ(ダイレクトメモリ)のサイズは、-XX:MaxDirectMemorySizeパラメータで指定できます。指定しない場合、デフォルトは最大Javaヒープ(-Xmxで指定)と同じです。直接メモリによるメモリオーバーフローの明らかな特徴は、ヒープダンプファイルに明らかな異常が見られないことです。メモリオーバーフロー後に生成されたダンプファイルが非常に小さいことがわかった場合、プログラムで直接または間接的に使用されます。 。DirectMemory(通常の間接的な使用はNIOです)の場合、直接メモリの理由の確認に焦点を当てることを検討できます。

 

参照:<Java仮想マシンの詳細な理解:JVMの高度な機能とベストプラクティス第3版>

 

おすすめ

転載: blog.csdn.net/qq_32323239/article/details/108742375