1。序文
Javaは、クロスハードウェアプラットフォームオブジェクト指向の高レベルプログラミング言語です。 Javaプログラムは、Java仮想マシン(JVM)で実行され、JVMによってメモリを管理します。これは、C ++との最大の違いです。メモリはJVMによって管理されていますが、JVMがメモリを管理する方法も理解する必要があります。 JVMは1つだけでなく、現在存在する仮想マシンが多い場合がありますが、仕様に準拠する仮想マシン設計では、「Java仮想マシン仕様」に従う必要があります。この記事は、Hotspot Virtual Machineの説明に基づいており、他の仮想マシンと違いがある場合に言及されます。この記事では、主にJVMでメモリがどのように分散されるか、Javaプログラムのオブジェクトがどのように保存およびアクセスされるか、およびさまざまなメモリ領域での可能な例外について説明します。
2。JVMのメモリ分布(領域)
JAVAプログラムを実行するとき、JVMはメモリを管理のために複数の異なるデータ領域に分割します。これらの領域には、異なる機能、創造、破壊時間があります。 JVMプロセスが開始されると割り当てられますが、他の領域はユーザースレッド(プログラム自体のスレッド)のライフサイクルに関連しています。 JVM仕様によると、JVMが管理するメモリ領域は、次のランタイムデータ領域に分割されます。
1。仮想マシンスタック
このメモリ領域はスレッドでプライベートであり、スレッドが起動して破壊されたときに破壊されると作成されます。仮想マシンスタックで記述されたJavaメソッドの実行のメモリモデル:各メソッドは、実行の開始時にスタックフレーム(スタックフレーム)を作成します。これは、ローカル変数テーブル、オペランドスタック、動的リンク、メソッドエグジなどを保存するために使用されます。各メソッドの実行とリターンが完了し、仮想マシンスタックにスタックフレームがあります。
名前が示すように、ローカル変数テーブルは、ローカル変数を格納するメモリ領域です。基本データ型(8つのJava基本データ型)、参照タイプ、およびコンパイラ期間中に見つけることができる返信アドレスを保存します。 64ビットを占める長い二重のタイプは、2つのローカル変数空間を占有し、他のデータ型は1のみを占有します。タイプサイズが決定され、コンパイル期間中に変数の数がわかるため、ローカル変数テーブルの作成時に既知のサイズがあります。メモリスペースのこの部分は、コンピレーション期間中に割り当てることができ、メソッドの実行中にローカル変数テーブルサイズを変更する必要はありません。
仮想マシンの仕様では、このメモリ領域に2つの例外が指定されています。
1.スレッドによって要求されたスタックの深さが許容深度(?)よりも大きい場合、 StackOverflowError例外がスローされます。
2.仮想マシンが動的に拡張できる場合、拡張が十分なメモリに適用できない場合、 OutOfMemory例外がスローされます。
2。ローカルメソッドスタック
ローカルメソッドスタックもスレッドプライベートであり、その機能は仮想マシンスタックとほぼ同じです。仮想マシンスタックはJavaメソッド実行のインおよびアウトスタックサービスを提供し、ローカルメソッドスタックは、ネイティブメソッドを実行するための仮想マシンのサービスを提供します。
仮想マシンの仕様では、ローカルメソッドスタックの実装方法に関する必須の規制はなく、特定の仮想マシンによって自由に実装できます。ホットスポット仮想マシンは、仮想マシンスタックを直接組み合わせ、ローカルメソッドは1つにスタックします。他の仮想マシンがこの方法を実装するために、読者は興味がある場合は関連情報を照会できます。
仮想マシンスタックと同様に、ローカルメソッドスタックは、 StackOverflowError和OutOfMemory例外もスローします。
3。プログラム計算機
プログラム計算機は、スレッドのプライベートメモリエリアでもあります。スレッドがbytecodeを実行するためのライン番号インジケーター(命令を指す)と見なすことができます。 Javaが実行されると、カウンターの値を変更することにより、次の命令が実行されます。ブランチ、ループ、ジャンプ、例外処理、スレッドリカバリなどの実行命令。すべてこのカウンターに依存して完了します。仮想マシンのマルチスレッドは、順番に切り替えてプロセッサの実行時間を割り当てることにより実現されます。プロセッサ(マルチコアプロセッサのコア)は、一度に1つのコマンドのみを実行できます。したがって、スレッドがスイッチングを実行した後、正しい実行位置に復元する必要があります。各スレッドには、独立したプログラム計算機があります。
Javaメソッドを実行するとき、プログラム計算機は、現在のスレッドが実行しているバイトコード命令のアドレスを記録(指します)。ネイティブメソッドが実行されている場合、この計算機の値は未定義です。これは、Hotspot Virtual Machineスレッドモデルがネイティブスレッドモデルであるため、各JavaスレッドはOSのスレッド(オペレーティングシステム)を直接マッピングするためです。ネイティブメソッドを実行すると、OSによって直接実行されます。仮想マシンのこのカウンターの値は役に立たない。この計算機は非常に小さなスペース、プライベートを備えたメモリ領域であり、拡張を必要としないためです。これは、 OutOfMemoryError例外を指定しない仮想マシン仕様の唯一の領域です。
4。ヒープメモリ(ヒープ)
Java Heapは、スレッドで共有されるメモリ領域です。仮想マシンが管理する最大のメモリ領域であり、仮想マシンが開始されたときに作成されると言えます。 Javaヒープメモリは主にオブジェクトインスタンスを保存し、ほぼすべてのオブジェクトインスタンス(配列を含む)がここに保存されます。したがって、これはゴミコレクション(GC)の主要メモリエリアでもあります。 GCに関するコンテンツはここでは説明されません。
仮想マシンの仕様によると、Javaヒープメモリは不連続な物理メモリにある可能性があります。論理的に連続的であり、スペース拡張に制限がない限り、固定サイズまたは拡張ツリーのいずれかにすることができます。ヒープメモリにインスタンス割り当てを完了するのに十分なスペースがなく、拡張できない場合、 OutOfMemoryError例外がスローされます。
5。メソッド領域
メソッド領域は、ヒープメモリと同様に、スレッドで共有されるメモリ領域です。タイプ情報、定数、静的変数、インスタントコンピレーション期間中にコンパイルされたコードを保存します。仮想マシンの仕様には、メソッド領域の実装にあまり多くの制限がありません。ヒープメモリと同様に、継続的な物理メモリスペースを必要とせず、サイズを固定またはスケーラブルにすることができ、ガベージコレクションを実装しないように選択することもできます。メソッド領域がメモリの割り当て要件を満たすことができない場合、 OutOfMemoryError例外がスローされます。
6。直接メモリ
直接メモリは、仮想マシンの管理されたメモリの一部ではありませんが、メモリのこの部分は頻繁に使用される場合があります。 Javaプログラムがネイティブメソッド(NIO、NIOなど、説明はありません)を使用すると、メモリは直接オフヒープで割り当てられる場合がありますが、メモリスペースの合計は限られており、メモリが不十分であり、 OutOfMemoryError例外もスローされます。
2。インスタンスオブジェクトストレージアクセス
上記の最初のポイントには、仮想マシンの各領域にメモリの一般的な説明があります。各エリアについて、データの作成、レイアウト、アクセス方法に問題があります。ホットスポットに基づいて、これらの3つの側面について話すために、最も一般的に使用されるヒープメモリを例として使用しましょう。
1.インスタンスオブジェクトの作成
仮想マシンが新しい命令を実行するとき、最初に、最初に定数プールから作成オブジェクトのクラスシンボル参照を見つけ、クラスがロードされ、初期化されているかどうかを判断します。ロードされていない場合、クラスの負荷初期化プロセスが実行されます(クラスの読み込みについて説明はここに行われません)。このクラスが見つからない場合、一般的なClassNotFoundException例外がスローされます。
クラスの読み込みチェック後、物理メモリ(ヒープメモリ)が実際にオブジェクトに割り当てられます。オブジェクトが必要とするメモリスペースは、対応するクラスによって決定されます。クラスの読み込み後、このクラスのオブジェクトに必要なメモリスペースが固定されます。オブジェクトにメモリスペースを割り当てることは、ピースをヒープから分割し、このオブジェクトに割り当てるのと同等です。
メモリスペースが連続しているかどうか(割り当ておよび未割り当てが2つの完全な部分に分割されているかどうか)に応じて、メモリを割り当てる2つの方法に分割されます。
1.連続メモリ:ポインターは、割り当てられたメモリと未割り当てメモリの間の分割点として使用されます。オブジェクトメモリの割り当てには、スペースサイズを未割り当てメモリセグメントに移動するためのポインターのみが必要です。この方法は「ポインター衝突」と呼ばれます。
2。不連続メモリ:仮想マシンは、割り当てられていないヒープ内のメモリブロックを記録するリストを維持(記録)する必要があります。オブジェクトメモリを割り当てるときは、適切なサイズのメモリ領域を選択してオブジェクトに割り当て、このリストを更新します。この方法は「フリーリスト」と呼ばれます。
オブジェクトメモリの割り当てには、同時実行の問題も発生します。仮想マシンは2つのソリューションを使用してこのスレッドの安全性の問題を解決します。まず、CAS(比較と設定)+を使用して識別および再試行して、割り当て操作の原子性を確保します。第二に、メモリの割り当ては、スレッドに応じて異なるスペースに分割されます。つまり、各スレッドは、ローカルスレッドに割り当てられたバッファー(TLAB)と呼ばれるヒープのスレッドプライベートメモリの一部を事前に割り当てました。そのスレッドがメモリを割り当てたい場合、TLABから直接割り当てられます。スレッドのTLABが再割り当て後に割り当てられた場合にのみ、同期操作をヒープから割り当てることができます。このソリューションは、スレッド間のオブジェクト割り当てヒープメモリの並行性を効果的に削減します。仮想マシンがTLABを使用するかどうかは、JVMパラメーター-XX:+/- USETLABを介して設定されています。
オブジェクトヘッダー情報に加えて、メモリの割り当てを完了した後、仮想マシンは割り当てられたメモリスペースをゼロ値に初期化し、オブジェクトインスタンスのフィールドを値を割り当てることなくデータ型に対応するゼロ値に直接使用できるようにします。次に、INITメソッドを実行して、インスタンスオブジェクトの作成が完了する前にコードに従って初期化を完了します。
2。メモリ内のオブジェクトのレイアウト
ホットスポット仮想マシンでは、オブジェクトはメモリ内の3つの部分に分割されます。オブジェクトヘッダー、インスタンスデータ、アライメントと充填:
オブジェクトヘッダーは2つの部分に分割されます。その一部は、ハッシュコード、ガベージコレクション生成年齢、オブジェクトロックステータス、スレッド保持ロック、バイアススレッドID、バイアスタイムスタンプなど、オブジェクトランタイムデータを保存します。 32ビットおよび64ビットの仮想マシンでは、データのこの部分はそれぞれ32ビットと64ビットを占めています。ランタイムデータが多数あるため、32ビットまたは64ビットではすべてのデータを完全に保存するのに十分ではないため、このパートはランタイムデータを固定されていない形式で保存するように設計されていますが、オブジェクトの状態に従ってデータを保存するために異なるビットを使用します。もう1つの部分には、このオブジェクトのクラスを指してオブジェクトタイプのポインターを保存しますが、これは必要ありません。オブジェクトのクラスメタデータは、ストレージのこの部分を使用して必ずしも決定する必要はありません(以下で説明します)。
インスタンスデータは、オブジェクトによって定義されたさまざまなタイプのデータの内容であり、これらのプログラムによって定義されたデータは定義された順序で保存されません。これらは、仮想マシンの割り当てポリシーと定義の順序で決定されます:ロング/ダブル、int、short/char、byte/boolean、oop(通常のオブジェクトポニント)は、ポリシーがタイプのプレースホルダーの数に従って割り当てられ、同じタイプがメモリを一緒に割り当てることがわかります。そして、これらの条件の満足の下で、親クラスの変数の順序の前にサブクラスがあります。
オブジェクトの充填パーツは必ずしも存在しません。それは、プレースホルダーのアライメントでのみ役割を果たします。ホットスポットでは、仮想マシンメモリ管理は8バイトの単位で管理されます。したがって、メモリが割り当てられると、オブジェクトサイズは8の倍数ではなく、アライメントフィリングが完了します。
3。オブジェクトアクセス<br /> Javaプログラムでは、オブジェクトを作成します。実際、参照型変数を取得し、実際にヒープメモリでインスタンスを操作します。仮想マシンの仕様では、参照タイプがオブジェクトを指す基準であると規定されており、この参照がヒープ内のインスタンスを見つけてアクセスする方法を指定していません。現在、主流の仮想マシンでは、オブジェクトアクセスを実装する主な方法が2つあります。
1。ハンドル方法:領域は、ハンドルプールとしてヒープメモリに分割されます。参照変数はオブジェクトのハンドルアドレスを保存し、ハンドルはサンプルオブジェクトとオブジェクトタイプの特定のアドレス情報を保存します。したがって、オブジェクトヘッダーにはオブジェクトタイプを含めることはできません。
2。ポインターへの直接アクセス:参照タイプは、ヒープ内のインスタンスオブジェクトのアドレス情報を直接保存しますが、これにはインスタンスオブジェクトのレイアウトにオブジェクトタイプを含める必要があります。
これらの2つのアクセス方法には、オブジェクトアドレスが変更された場合(メモリソート、ガベージコレクション)、ハンドルアクセスオブジェクトを変更すると、参照変数を変更する必要はありませんが、ハンドル内のオブジェクトアドレス値のみが変更されます。 Pointer Direct Accessメソッドを使用している間、このオブジェクトのすべての参照を変更する必要があります。しかし、ポインター方法は1つのアドレス指定操作を減らすことができ、多数のオブジェクトアクセスの場合、この方法の利点はより明白です。 Hotspot Virtual Machineは、このポインターダイレクトアクセス方法を使用します。
3。ランタイムメモリの例外
Javaプログラムで実行するときに発生する可能性のある2つの主な例外があります。OutofMemoryErrorとStackoverFlowerror。そのメモリエリアはどうなりますか?前に前述したように、プログラムカウンターを除き、他のメモリ領域が発生します。このセクションでは、主にインスタンスコードを介した各メモリ領域の例外を示しており、一般的に使用される多くの仮想マシンスタートアップパラメーターを使用して、状況をよりよく説明します。 (パラメーターでプログラムを実行する方法はここでは説明されていません)
1。Javaヒープメモリオーバーフロー
ヒープメモリオーバーフローは、ヒープ容量が最大ヒープ容量に達した後にオブジェクトが作成されると発生します。プログラムでは、オブジェクトは継続的に作成され、これらのオブジェクトはごみ収集されないことが保証されています。
/** *仮想マシンパラメーター: * -xms20m最小ヒープ容量 * -xmx20m最大ヒープ容量 * @author hwz * * */public class headoutofmemoryerror {public static void main(// containerを使用するためにコンテナを使用してオブジェクトが保存していることを確認arrayList <headOutofMemoryError>(); while(true){//オブジェクトを継続的に作成し、それらをコンテナリストに追加しますtoholdobj.add(new HeadOutofMemoryError()); }}}仮想マシンパラメーター:-XX:HeapDumpOnOutOfMemoryErrorを追加できます。 OOM例外を送信するときは、仮想マシンに現在のヒープのスナップショットファイルをダンプします。このファイルの単語セグメンテーション例外の問題を将来使用できます。これについては詳細に説明しません。メモリの問題を分析するためにMATツールを使用して詳細に説明するブログを作成します。
2。仮想マシンスタックとローカルメソッドスタックオーバーフロー
Hotspot Virtual Machineでは、これら2つのメソッドスタックが一緒に実装されていません。仮想マシンの仕様によると、これら2つの例外は、これら2つのメモリ領域で発生します。
1.スレッドが仮想マシンで許可された最大深度よりも大きいスタック深度を要求する場合、StackOverFlowerrorの例外を投げます。
2.スタックスペースを拡張するときに仮想マシンが大きなメモリスペースに適用できない場合、OutFmeMoryErrorの例外がスローされます。
これら2つの状況の間には実際に重複しています。スタックスペースを割り当てることができない場合、メモリが小さすぎるか、使用されているスタックの深さが大きすぎるかを区別することは不可能です。
2つの方法を使用してコードをテストします
1. -XSSパラメーターを使用してスタックサイズを減らし、メソッドを無限に再帰的に呼び出し、スタックの深さを無限に増やします。
/** *仮想マシンパラメーター:<br> * -XSS128Kスタック容量 * @author hwz * */public class stackoverflowerror {private int stackdeep = 1; / ***無限再帰、コールスタックの深さを無限に拡大する*/ public void recursiveinvoke(){stackdeep ++; recursiveinvoke(); } public static void main(string [] args){stackoverflowerror soe = new stackoverflowerror(); try {soe.recursiveinvoke(); } catch(throwable e){system.out.println( "stack deep =" + soe.stackdeep); eを投げる; }}}メソッドでは多数のローカル変数が定義されています。メソッドスタックのローカル変数テーブルの長さは、無限に再帰的に呼ばれます。
/** * @author hwz * */public class stackoomeerror {private int stackdeep = 1; / ***多数のローカル変数を定義し、スタック内のローカル変数テーブルを増やします*無限再帰、コールスタックの深さを無限に増加させる*/ public void recursiveinvoke(){double i;ダブルI2; //......... StackDeep ++では、多数の可変定義が省略されています。 recursiveinvoke(); } public static void main(string [] args){stackoomeerror soe = new stackoomeerror(); try {soe.recursiveinvoke(); } catch(throwable e){system.out.println( "stack deep =" + soe.stackdeep); eを投げる; }}}上記のコードテストでは、フレームスタックが大きすぎるか、仮想マシン容量が小さすぎるかどうかに関係なく、メモリを割り当てることができない場合、すべてのStackOverFlowerrorがスローされます。
3.メソッドエリアとランタイム定数プールオーバーフロー
ここでは、最初に文字列のインターン方法について説明します。文字列定数プールに既にこの文字列オブジェクトに等しい文字列が含まれている場合、この文字列を表す文字列オブジェクトを返します。それ以外の場合は、この文字列オブジェクトを定数プールに追加し、この文字列オブジェクトへの参照を返します。この方法により、一定のプールに文字列オブジェクトを継続的に追加し、オーバーフローになります。
/** *仮想マシンパラメーター:<br> * -xx:permsize = 10m永続的なエリアサイズ * -xx:maxpermsize = 10m永久領域最大容量 * @author hwz * */public class runtimeconstancepooloom {public static void main(string [] args){//オブジェクトを保存するためにオブジェクトを保存するためにコンテナを使用します> arrayList <String>(); // string.internメソッドを使用して、(int i = 1; true; i ++){list.add(string.valueof(i).intern())の定数プールのオブジェクトを追加します。 }}}ただし、このテストコードは、JDK1.7のランタイム定数プール中にオーバーフローしませんが、JDK1.6で発生します。このため、この問題を確認するために別のテストコードを記述します。
/** string.internメソッドは異なるjdks * @author hwz * */public class stringinterntest {public static void main(string [] args){string str1 = new stringbuilder( "test")。 System.out.println(str1.intern()== str1); string str2 = new StringBuilder( "test")。append( "02")。toString(); System.out.println(str2.intern()== str2); }} JDK1.6の下で実行された結果は次のとおりです。
JDK1.7の下で実行された結果は次のとおりです。
JDK1.6では、インターン()メソッドは、最初の遭遇した文字列インスタンスを永続的な世代にコピーします。これは、永続的な世代のインスタンスへの参照であり、StringBuilderによって作成された文字列インスタンスはヒープにあるため、等しくありません。
JDK1.7では、インターン()メソッドはインスタンスをコピーするのではなく、定数プールに表示される最初のインスタンスの参照のみを記録します。したがって、インターンによって返される参照は、StringBuilderによって作成されたインスタンスと同じであるため、trueを返します。
したがって、一定のプールオーバーフローのテストコードは、一定のプールオーバーフローの例外を持つことはありませんが、連続的に実行後にはヒープメモリのオーバーフロー例外が不十分な場合があります。
次に、メソッド領域のオーバーフローをテストする必要があります。クラス名、アクセス修飾子、一定のプールなど、メソッド領域に物事を追加し続けるだけです。プログラムに多数のクラスをロードして、メソッド領域を継続的に埋めることができます。 CGLIBを使用して、バイトコードを直接操作して、多数の動的クラスを生成します。
/** *メソッドメモリオーバーフローテストクラス * @author hwz * */public class methodareaoom {public static void main(string [] args){// gclibを使用して、サブクラスを無限に作成します(true){enthancer enthancer = new enthancer(); Enhancer.setsuperclass(maoomclass.class); Enhancer.SetUseCache(false); Enhancer.setCallback(new MethodEnterceptor(){@Override public object intercept(Object obj、Method Method、Object args、MethodProxy Proxy)スロー可能{return proxy.invokesuper(obj、args);}}); Enhancer.create(); }} static class maoomclass {}} VisualVM観測を通じて、JVMロードされたクラスの数がPergenを使用すると直線で増加することがわかります。
4。直接メモリオーバーフロー
直接メモリのサイズは、仮想マシンパラメーターを介して設定できます。-xx:maxdirectmemorysize 。直接メモリオーバーフローを行うには、直接メモリを継続的に適用するだけです。以下は、Java nioの直接メモリキャッシュテストと同じです。
/** *仮想マシンパラメーター:<br> * -xx:maxdirectmememorysize = 30mダイレクトメモリサイズ * @author hwz * */public class directmemoryoom {public static void main(string [] args){list <buffer> buffers = new raylist <buffer>(); int i = 0; while(true){//現在のSystem.out.println(++ i)を印刷します。 //キャッシュバッファーで直接バッファメモリ消費を継続的に適用することによる直接メモリ消費(bytebuffer.allocatedirect(1024*1024)); //毎回1mをアカウンティングします}}}ループでは、1mの直接メモリが適用されるたびに、最大直接メモリが30mに設定され、プログラムが31回実行されると例外がスローされます: java.lang.OutOfMemoryError: Direct buffer memory
4。概要
上記はこの記事のすべての内容です。この記事では、主に、JVMのさまざまなメモリ領域で発生する可能性のあるメモリ、オブジェクトストレージ、およびメモリの例外のレイアウト構造について説明します。メイン参照帳「Java Virtual Machine(第2版)の詳細な理解」。間違ったものがある場合は、コメントで指摘してください。 wulin.comへのご支援ありがとうございます。