仮想マシンスタックは、JVMメモリ構造内のスレッドプライベートモジュールの1つです。この機能は、後入れ先出しです。この機能は、メソッドの呼び出しプロセスがスタックで実行されることを決定します。メソッドが呼び出されるたびに、このメソッドに対応するスタックフレームがスタックに生成されます。スタックフレームには、ローカル変数テーブル、オペランドスタック、動的接続、およびメソッドリターンアドレスの4つの部分が含まれます。スタックのメモリが十分なスタックフレームを保持するのに十分でない場合、つまり、いわゆるスタック試行が仮想許容深度よりも大きい場合、StackOverFlowError例外がスローされます。スタックが拡張され、JVMメモリが不足している場合、OutOfMemoryError例外がスローされます。次に、スタックはグラフィックスと特定のコードを通じて導入されます
スタック構造
スタックの構造を次の図に示します
。Method1とMethod2は2つのスタックフレームに対応しており、この図から、Method1がMethod2で呼び出されていることがわかります。各スタックフレームには、ローカル変数テーブル、オペランドスタック、動的リンク、メソッドリターンアドレスの4つのコア部分があります。もちろん、ここでは紹介されていない他の部分もあります。
ローカル変数テーブル
名前が示すように、ローカル変数を格納するために使用されます。つまり、メソッド内のすべての変数がこのテーブルに格納され、各変数は実行時に割り当てられます。
オペランドスタック
これは、メソッド内のすべての基本型変数の値、つまりローカル変数テーブルの変数に対応する特定の値を格納するために使用されます。
動的リンク
これは、定数プール内の呼び出しメソッドを指すシンボリック参照を、呼び出しメソッドの直接参照に変換するために使用されます。
メソッドの戻りアドレス
これは、このメソッドが呼び出される場所を格納するために使用される行番号として理解できます。例:Method2は7行目でMethod1を呼び出し、Method1のスタックフレーム内の対応するメソッドリターンアドレスは7です。もちろん、このように理解できますが、実際にはアドレスのセクションです。
コード分析
上記の紹介により、一般的な理解はありますが、あいまいな点があるかもしれません。次に、コードを使って分析します。
public class JVMTest {
public int add(){
int a = 3;
int b = 10;
int c = a+b;
return c;
}
public static void main(String[] args) {
JVMTest jvmTest = new JVMTest();
int result = jvmTest.add();
System.out.println("The result is : "+result);
}
}
このコードは非常に単純です。クラスにはmainメソッドとaddメソッドの2つのメソッドがあり、addメソッドはmainメソッドで呼び出され、addメソッドは単純な加算操作を実行します。では、仮想マシンスタック内のこのコードは実行時にどのように機能しますか?このコードのバイトコードファイルの分析を次に示します。バイトコードファイルは、javapコマンドを使用して読み取り可能な命令コードに変換できます。javap -c JVMTest.class
以下の結果によるコードが得られました。
Compiled from "JVMTest.java"
public class com.research.JVMTest {
public com.research.JVMTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int add();
Code:
0: iconst_3
1: istore_1
2: bipush 10
4: istore_2
5: iload_1
6: iload_2
7: iadd
8: istore_3
9: iload_3
10: ireturn
public static void main(java.lang.String[]);
Code:
0: new #2 // class com/research/JVMTest
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method add:()I
12: istore_2
13: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
16: iload_2
17: invokedynamic #6, 0 // InvokeDynamic #0:makeConcatWithConstants:(I)Ljava/lang/String;
22: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
25: return
}
頭痛の種のように見えますが、気楽に、心配しないでください。ここでは、主にaddメソッドとmainメソッドを使用して、スタックの4つのコア領域がどのように機能するかを分析します。分析の前に、説明を読む必要があります。覚えておく必要はありません。読むときは、公式のjvm取扱説明書から問い合わせることができます。ここでは、addメソッドの10個の命令を次のように整理します。
命令 | 効果 | 範囲 |
---|---|---|
iconst_3 | int型定数値3がオペランドスタックに入ります | オペランドスタック |
istore_1 | スタックの一番上にあるint値を最初のローカル変数に格納します | オペランドスタックからローカル変数テーブルへ |
ビプッシュ | 1バイトの定数値(-128〜127)をスタックの一番上にプッシュします | オペランドスタック |
istore_2 | スタックの一番上にあるint値を2番目のローカル変数に格納します | オペランドスタックからローカル変数テーブルへ |
iload_1 | 最初のint型ローカル変数をオペランドスタックの一番上にプッシュします | ローカル変数テーブルからオペランドスタックへ |
iload_2 | 2番目のint型ローカル変数をオペランドスタックの一番上にプッシュします | ローカル変数テーブルからオペランドスタックへ |
私は追加します | スタックの一番上に2つのint値を追加し、その結果をオペランドスタックの一番上にプッシュします | オペランドスタック |
istore_3 | スタックの一番上にあるint値を3番目のローカル変数に格納します | オペランドスタックからローカル変数テーブルへ |
iload_3 | 3番目のint型ローカル変数をオペランドスタックの一番上にプッシュします | ローカル変数テーブルからオペランドスタックへ |
私は返す | 現在のメソッドからintを返す | オペランドスタック |
PS:大学でアセンブリを学んだ学生は、これらの命令が私に馴染みがあると感じるはずです。もちろん、
addメソッド全体の実行プロセスも次の図に示されていると思います。各ブロックは命令を表し、命令ははスタックフレームを示しています。ローカル変数テーブルとオペランドスタックがメモリの一部に対応するさまざまな部分の動的な変化。オペランドスタックは、変数と計算結果を一時的に格納する一時的なストレージ領域であり、メソッドはアドレスは、このメソッドの出口として使用されます。
addメソッドの定義がすべて基本型変数であることを見つけるのは難しくありません。定義が参照型変数の場合、ローカル変数テーブルで何が起こりますか。mainメソッドを見てください:
mainメソッドは参照型のローカル変数を定義します。その値はオブジェクトを指すアドレスです。もちろん、このオブジェクトはオペランドスタックに格納されませんが、ヒープに格納されます(浅いものからJVM -Heapのより深い理解)。この時点で、仮想マシンスタックとヒープの間にブリッジを構築できます。
ほとんど光が見えており、最後のコア部分であるダイナミックリンクが残っています。動的リンクなので、動的な方法はどうですか。
Javaソースファイルがバイトコードファイルにコンパイルされると、すべての変数とメソッド参照は、クラスファイルの定数プール(メソッド領域)にシンボリック参照として保存されます。たとえば、別のメソッドを呼び出すメソッドを記述する場合、定数プール内のメソッドへのシンボル参照(上記のバイトコードメインメソッド内17: invokedynamic #6, 0
)で表されます。動的リンクの機能は、これらのシンボル参照をに変換することです。呼び出し元のメソッドの直接参照。
この時点で、仮想スタックの動作原理が導入されました。他の部分の動作原理については、次のブログ投稿を参照して、
JVM-
浅いものから深いものへのメモリ構造を理解してください。JVM-浅いものから深いものへのヒープ
を理解してください。 JVMプログラムカウンタ