JVM シリーズ - JVM メモリ構造#
内容来自:
基本概念#
Java 仮想マシンのメモリ空間は 5 つの部分に分かれています:
- プログラムカウンタ
- Java 仮想マシンスタック
- ネイティブメソッドスタック
- ヒープ
- メソッド領域
『深入理解 Java 仮想機』の図を引用:
プログラムカウンタ (PC レジスタ)#
プログラムカウンタの定義#
プログラムカウンタは小さなメモリ空間であり、現在のスレッドが実行中のバイトコード命令のアドレスです。現在のスレッドがネイティブメソッドを実行している場合、この時プログラムカウンタは未定義です。
プログラムカウンタの役割#
- バイトコードインタプリタはプログラムカウンタを変更することで命令を順次読み取り、コードのフロー制御を実現します。
- マルチスレッドの場合、プログラムカウンタは現在のスレッドの実行位置を記録し、スレッドが切り替わった際に前回のスレッドがどこまで実行されたかを知ることができます。
プログラムカウンタの特徴#
- 小さなメモリ空間です。
- スレッド専用であり、各スレッドには独自のプログラムカウンタがあります。
- ライフサイクル:スレッドの作成に伴い作成され、スレッドの終了に伴い破棄されます。
- OutOfMemoryError が発生しない唯一のメモリ領域です。
Java 仮想マシンスタック#
Java 仮想マシンスタックの定義#
Java 仮想マシンスタックは Java メソッドの実行過程を記述するメモリモデルです。
Java 仮想マシンスタックは、実行予定の各 Java メソッドのために「スタックフレーム」と呼ばれる領域を作成し、そのメソッドの実行過程に関する情報を格納します。以下の図のように:
Java 仮想マシンスタックの特徴#
- ローカル変数テーブルはスタックフレームの作成に伴い作成され、そのサイズはコンパイル時に決定されます。作成時には事前に規定されたサイズを割り当てるだけで済みます。メソッド実行中にローカル変数テーブルのサイズは変更されません。
- Java 仮想マシンスタックでは 2 種類の例外が発生します:StackOverFlowError と OutOfMemoryError。
- StackOverFlowError は、Java 仮想マシンスタックのサイズが動的に拡張できない場合、スレッドがスタックの深さを現在の Java 仮想マシンスタックの最大深さを超えた時に発生します(StackOverFlowError が発生した場合、メモリ空間はまだ多く残っている可能性があります)。
- OutOfMemoryError は、動的拡張が許可されている場合、スレッドがスタックを要求した際にメモリが使い果たされ、さらに動的に拡張できない場合に発生します。
- Java 仮想マシンスタックもスレッド専用であり、スレッドの作成に伴い作成され、スレッドの終了に伴い破棄されます。データはスレッド間で共有されないため、データの整合性問題を気にする必要はなく、同期ロックの問題も存在しません。
ネイティブメソッドスタック(C スタック)#
Java が C または C++ で書かれたインターフェースサービスを使用する際、コードはこの領域で実行されます。
ネイティブメソッドスタックは JVM がネイティブメソッドを実行するためのスペースであり、多くのネイティブメソッドが C 言語で実装されているため、通常は C スタックとも呼ばれます。これは Java 仮想マシンスタックと同様の機能を持ちますが、ネイティブメソッドスタックはネイティブメソッドの実行過程を記述するメモリモデルです。
ヒープ#
ヒープの定義#
ヒープはオブジェクトを格納するためのメモリ空間であり、ほぼすべてのオブジェクトがヒープに格納されます。ヒープメモリは新生代、老年代、永続代に分かれています。新生代はさらにEden 領域+Survior1 領域+Survior2 領域に分かれています。JDK 1.8 では永続代が完全に削除され、代わりにメタスペース(Metaspace)** と呼ばれる領域が導入されました(永続代は JVM のヒープメモリ空間を使用し、メタスペースは物理メモリを使用し、直接的にホストの物理メモリ制限を受けます)。
ヒープの特徴#
- スレッド共有:Java 仮想マシン全体で 1 つのヒープしかなく、すべてのスレッドが同じヒープにアクセスします。一方、プログラムカウンタ、Java 仮想マシンスタック、ネイティブメソッドスタックは各スレッドに対応しています。
- 仮想マシンの起動時に作成されます。
- ガベージコレクションの主要な場所です。
異なる領域には異なるライフサイクルのオブジェクトが格納されており、異なる領域に応じて異なるガベージコレクションアルゴリズムを使用することで、よりターゲットを絞った処理が可能です。
ヒープのサイズは固定または拡張可能ですが、主流の仮想マシンではヒープのサイズは拡張可能であるため、スレッドがメモリの割り当てを要求した際にヒープが満杯で、メモリがこれ以上拡張できない場合、OutOfMemoryError 例外が発生します。
Java ヒープが使用するメモリは連続している必要はありません。また、ヒープはすべてのスレッドで共有されているため、そのアクセスには同期の問題に注意が必要であり、メソッドと対応する属性は整合性を保証する必要があります。
メソッド領域#
メソッド領域の定義#
Java 仮想マシン仕様では、メソッド領域はヒープの論理部分と定義されています。メソッド領域には以下の情報が格納されます:
- 仮想マシンにロードされたクラス情報
- 定数
- 静的変数
- JIT コンパイラによってコンパイルされたコード
メソッド領域の特徴#
- スレッド共有:メソッド領域はヒープの論理部分であるため、ヒープと同様にスレッド共有です。仮想マシン全体で 1 つのメソッド領域しかありません。
- 永続代:メソッド領域の情報は一般的に長期間存在する必要があり、またヒープの論理区分であるため、ヒープの区分方法を用いてメソッド領域を「永続代」と呼びます。
- メモリ回収効率は低い。メソッド領域の情報は一般的に長期間存在する必要があり、一度回収された後は無効な情報が少量しか残らない可能性があります。主な回収対象は:定数プールの回収;型のアンロードです。
- Java 仮想マシン仕様はメソッド領域に対する要求が比較的緩やかです。ヒープと同様に、固定サイズを許可し、動的拡張を許可し、ガベージコレクションを実装しないことも許可されています。
実行時定数プール#
メソッド領域には:クラス情報、定数、静的変数、JIT コンパイラによってコンパイルされたコードが格納されます。定数は実行時定数プールに格納されます。
クラスが Java 仮想マシンにロードされると、.class ファイル内の定数はメソッド領域の実行時定数プールに格納されます。また、実行中に新しい定数を定数プールに追加することもできます。例えば、String クラスの intern () メソッドは実行中に定数プールに文字列定数を追加できます。
JVM 制御パラメータ#
図のように、一般的な制御パラメータは以下の通りです:
-
-Xms
ヒープの最小空間サイズを設定します。 -
-Xmx
ヒープの最大空間サイズを設定します。 -
-XX:NewSize
新生代の最小空間サイズを設定します。 -
-XX:MaxNewSize
新生代の最大空間サイズを設定します。 -
-XX:PermSize
永続代の最小空間サイズを設定します。 -
-XX:MaxPermSize
永続代の最大空間サイズを設定します。 -
-Xss
各スレッドのスタックサイズを設定します。
老年代のサイズを直接設定するパラメータはありませんが、ヒープ空間サイズと新生代空間サイズの 2 つのパラメータを設定することで間接的に制御できます。老年代空間サイズ = ヒープ空間サイズ - 若い代の大きな空間サイズです。