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 仮想マシンには一つのヒープしかありません。すべてのスレッドが同じヒープにアクセスします。一方、プログラムカウンタ、Java 仮想マシンスタック、ネイティブメソッドスタックはそれぞれのスレッドに対応しています。
- 仮想マシン起動時に作成されます。
- ガベージコレクションの主要な場所です。
異なる領域には異なるライフサイクルのオブジェクトが格納されており、異なる領域に応じて異なるガベージコレクションアルゴリズムを使用でき、よりターゲットを絞ったものになります。
ヒープのサイズは固定も拡張も可能ですが、主流の仮想マシンではヒープのサイズは拡張可能であるため、スレッドがメモリを割り当てることを要求したときにヒープが満杯で、メモリがこれ以上拡張できない場合、OutOfMemoryError 例外がスローされます。
Java ヒープが使用するメモリは連続している必要はありません。また、ヒープはすべてのスレッドで共有されているため、アクセス時には同期の問題に注意が必要であり、メソッドと対応する属性は整合性を保証する必要があります。
メソッド領域#
メソッド領域の定義#
Java 仮想マシン規範では、メソッド領域はヒープの論理部分と定義されています。メソッド領域には以下の情報が格納されます:
- 仮想マシンにロードされたクラス情報
- 定数
- 静的変数
- JIT コンパイラによってコンパイルされたコード
メソッド領域の特徴#
- スレッド共有: メソッド領域はヒープの論理部分であるため、ヒープと同様にスレッド共有です。全仮想マシンにおいてメソッド領域は一つしかありません。
- 永続代: メソッド領域の情報は一般に長期間存在する必要があり、またヒープの論理区分であるため、ヒープの区分方法を用いてメソッド領域を「永続代」と呼びます。
- メモリ回収効率が低い。メソッド領域の情報は一般に長期間存在する必要があり、一度回収した後は無効な情報が少量しか残らないことが多いです。主な回収対象は:定数プールの回収;型のアンロード。
- Java 仮想マシン規範はメソッド領域に対する要求が比較的緩やかです。ヒープと同様に、固定サイズを許可し、動的拡張も許可し、ガベージコレクションを実装しないことも許可されています。
実行時定数プール#
メソッド領域には:クラス情報、定数、静的変数、JIT コンパイラによってコンパイルされたコードが格納されます。定数は実行時定数プールに格納されます。
クラスが Java 仮想マシンにロードされると、.class ファイル内の定数はメソッド領域の実行時定数プールに格納されます。また、実行中に新しい定数を定数プールに追加することもできます。例えば、String クラスの intern () メソッドは、実行中に定数プールに文字列定数を追加することができます。
JVM 制御パラメータ#
図に示すように、一般的な制御パラメータは以下の通りです:
-
-Xms
ヒープの最小空間サイズを設定します。 -
-Xmx
ヒープの最大空間サイズを設定します。 -
-XX:NewSize
新生代の最小空間サイズを設定します。 -
-XX:MaxNewSize
新生代の最大空間サイズを設定します。 -
-XX:PermSize
永続代の最小空間サイズを設定します。 -
-XX:MaxPermSize
永続代の最大空間サイズを設定します。 -
-Xss
各スレッドのスタックサイズを設定します。
老年代のサイズを直接設定するパラメータはありませんが、ヒープ空間のサイズと新生代空間のサイズの 2 つのパラメータを設定することで間接的に制御できます。老年代空間のサイズ = ヒープ空間のサイズ - 若い代の大空間のサイズ。