仮想マシンスタックの背景
プラットフォーム非依存性の設計により、Javaの命令セットはスタックベースで設計されています。異なるプラットフォームではCPUアーキテクチャが異なるため、レジスタベースにすることはできません。
スタックベース設計の利点はプラットフォーム非依存性、小さな命令セット、コンパイラの実装の容易さですが、欠点はパフォーマンスの低下であり、同じ機能を実現するためにより多くの命令が必要になることです。
メモリ内のヒープとスタック
スタックは実行時の単位であり、ヒープはストレージの単位です
1.スタックはプログラムの実行問題、つまりプログラムがどのように実行されるか、あるいはデータがどのように処理されるかを解決します。ヒープはデータの保存問題、つまりデータをどのように配置し、どこに配置するかを解決します。
2.一般的に、オブジェクトは主にヒープ領域に配置され、これはランタイムデータ領域の中で比較的大きな部分です。
3.スタック領域には、基本データ型のローカル変数と、参照データ型のオブジェクトへの参照が格納されます。
Java仮想マシンスタックの特徴
1.Java仮想マシンスタック(Java Virtual Machine Stack)は、以前にはJavaスタックとも呼ばれていました。各スレッドは作成時に仮想マシンスタックを作成し、その内部にはスタックフレーム(Stack Frame)が保存されています。これはJavaメソッド呼び出しに対応しています。これはスレッドごとに固有のものです。
2.ライフサイクルはスレッドと一致します。
3.スタックは高速で効率的なストレージ割り当て方式であり、アクセス速度はPCレジスタ(プログラムカウンタ)に次いで高速です。
4.機能:Javaプログラムの実行を主管し、メソッドのローカル変数、8種類の基本データ型、オブジェクトの参照アドレス、一部の結果を保存し、メソッドの呼び出しと戻りに参与します。
ローカル変数:メンバ変数(メンバ変数またはプロパティ)と比較して
基本データ変数:8種類の基本データ型
参照型変数:クラス、配列、インターフェース
5.JVMがJavaスタックに対して直接行う操作は2つだけです(1)各メソッドの実行は、スタックへのプッシュ(プッシュ、プッシュスタック)と共に伴います
(2)実行終了後のポップ操作
6.スタックではガベージコレクションの問題は存在しませんが、OutOfMemoryError(OOM)例外が確実に存在します
次にJava仮想マシンスタックの例外について説明します
スタックで発生し得る例外
Java仮想マシン仕様では、Javaスタックのサイズは動的であるか、固定不变であることを許可しています
固定サイズのJava仮想マシンスタックが採用されている場合、各スレッドのJava仮想マシンスタック容量はスレッド作成時に個別に選択できます。スレッドが要求するスタック容量がJava仮想マシンスタックが許容する最大容量を超えた場合、Java仮想マシンはStackOverFlowError例外をスローします
/**
* スタック内の例外をデモンストレーション
*/
public class StackErrorDemo {
public static void main(String[] args) {
main(args);
}
}
Java仮想マシンスタックが動的に拡張でき、拡張を試みた際に十分なメモリを確保できない場合、または新しいスレッドを作成する際に対応する仮想マシンスタックを作成するのに十分なメモリがない場合、Java仮想マシンはOutOfMemoryError例外をスローします
スタックのメモリサイズ設定
-Xssオプションを使用してスレッドの最大スタックスペースを設定できます。スタックのサイズは、関数呼び出しの最大到達深度を直接決定します。(IDEAでの設定方法:Run-EditConfigurations-VM optionsにスタックサイズを指定-Xss256kを入力)
/**
* スタック内の例外をデモンストレーション
*
* デフォルトの場合:count 10818
* スタックサイズ設定: -Xss256k count 1872
*/
public class StackErrorDemo {
private static int counter = 1;
public static void main(String[] args) {
System.out.println(counter);
counter++;
main(args);
}
}
Java仮想マシンスタックのストレージ構造と実行原理
1.各スレッドには独自のスタックがあり、スタック内のデータはすべて**スタックフレーム(Stack Frame)**の形式で存在します
2.このスレッドで実行中の各メソッドは、それぞれ独自のスタックフレームに対応しています
3.スタックフレームはメモリブロックであり、データセットであり、メソッド実行プロセス中の各種データ情報を維持しています
4.JVMがJavaスタックに対して直接行う操作は2つだけで、それはスタックフレームのプッシュとポップであり、先入れ後出し(LIFO)の原則に従います。
5.アクティブなスレッドでは、ある時点でアクティブなスタックフレームは1つだけです。つまり、現在実行中のメソッドのスタックフレーム(スタックトップフレーム)のみが有効であり、このスタックフレームは**現在のスタックフレーム(Current Frame)と呼ばれ、これに対応するメソッドが現在のメソッド(Current Method)**です
6.実行エンジンが実行するすべてのバイトコード命令は、現在のスタックフレームに対する操作のみです
7.このメソッドで他のメソッドが呼び出された場合、対応する新しいスタックフレームが作成され、スタックのトップに配置され、新しい現在のスタックフレームになります。
8.異なるスレッドに含まれるスタックフレームは相互に参照することはできません。つまり、あるスタックフレームから別のスレッドのスタックフレームを参照することは不可能です
9.現在のメソッドが他のメソッドを呼び出した場合、メソッドが戻る際に、現在のスタックフレームは実行結果を前のスタックフレームに渡し、次に仮想マシンは現在のスタックフレームを破棄し、前のスタックフレームが再び現在のスタックフレームになります
10.Javaメソッドには2つの戻り方式があり、1つは通常の関数戻りでreturn命令を使用するもの、もう1つは例外をスローするものです。どちらの方式を使用する場合でも、スタックフレームがポップされます。
コード例:
/**
* スタックフレーム
*/
public class StackFrameDemo {
public static void main(String[] args) {
StackFrameDemo demo = new StackFrameDemo();
demo.process1();
//出力:process1()とprocess2()はそれぞれ2回現在のスタックフレームとして現れ、process3()は1回
// process1()の実行開始。。。
// process2()の実行開始。。。
// process3()の実行開始。。。
// process3()の実行終了。。。
// process2()の実行終了。。。
// process1()の実行終了。。。
}
public void process1(){
System.out.println("process1()の実行開始。。。");
process2();
System.out.println("process1()の実行終了。。。");
}
public int process2(){
System.out.println("process2()の実行開始。。。");
int value = 10;
int result = (int) process3();
System.out.println("process2()の実行終了。。。");
return value + result;
}
public double process3(){
System.out.println("process3()の実行開始。。。");
double number = 20.0;
System.out.println("process3()の実行終了。。。");
return number;
}
}
仮想マシンスタックに関する面接問題
1.スタックオーバーフローの状況の例?(StackOverflowError)
- 再帰呼び出しなど、-Xssでスタックサイズを設定;
2.スタックサイズを調整すれば、オーバーフローを防げるようになるか?
- いいえ。例えば無限回の再帰は必ずオーバーフローする。スタックサイズを調整してもオーバーフローが発生する時間が遅くなるだけです。極端な場合はOOMメモリ不足(Out Of Memory Error)につながります。注意すべきはErrorであること
3.割り当てられたスタックメモリが大きいほど良いか?
- いいえ。他のスレッドのスペースを圧迫します
4.ガベージコレクションは仮想マシンスタックに関与しますか?- しません
Errorについてもう少し説明しましょう。上記の議論はExceptionを含みません。 まず、ExceptionとErrorは両方ともThrowableクラスを継承しています。JavaではThrowable型のインスタンスのみがスロー(throw)またはキャッチ(catch)できます。これは例外処理メカニズムの基本的な構成要素です。 ExceptionとErrorは、JAVA言語が例外処理に対して示す2つの方法を体現しています。 ExceptionはJavaプログラム実行中に予測可能な異常状況であり、この種の例外を取得し、ビジネス外の処理を行うことができます。 ErrorはJavaプログラム実行中に予測不可能な異常状況であり、この種の例外が発生すると、JVMが処理不能または回復不能な状態になります。したがって、この種の例外は捕捉できません。例えばOutOfMemoryError、NoClassDefFoundErrorなどです。 その中のExceptionは、さらに検査例外と非検査例外に分けられます。2つの根本的な違いは、検査例外はコード作成時にtry catchを使用して捕捉しなければならないこと(例:IOException例外)、非検査例外はコード作成時にキャッチ操作を無視できること(例:ArrayIndexOutOfBoundsException)であり、これはコード作成または使用プロセスで規範を通じて発生を避けることができます。
5.メソッドで定義されたローカル変数はスレッドセーフですか?
- 場合による分析が必要です
/**
* 面接問題:
* メソッドで定義されたローカル変数はスレッドセーフですか?場合による分析が必要です
*
* 何がスレッドセーフか?
* このデータを操作できるスレッドが1つしかない場合、必ずスレッドセーフです。
* 複数のスレッドがこのデータを操作する場合、このデータは共有データです。同期メカニズムを考慮しない場合、スレッドセーフの問題が存在します
*
* StringBufferはスレッドセーフであることは知っています。ソースコードではsynchronizedが実装されています。StringBuilderのソースコードではsynchronizedが実装されておらず、マルチスレッド状況では安全ではありません<br></br> * 両者ともAbstractStringBuilderから継承しています<br></br> *<br></br> */
public class StringBuilderDemo {
//s1の宣言方法はスレッドセーフです。s1はmethod1内部で消滅します
public static void method1(){
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
}
//StringBuilderの操作プロセス:method2が複数のスレッドから呼び出される可能性があるため、安全ではありません
public static void method2(StringBuilder builder){
builder.append("a");
builder.append("b");
}
//s1の操作:戻り値があるため、他のスレッドと共有される可能性があり、安全ではありません
public static StringBuilder method3(){
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
return s1;
}
//s1の操作:StringBuilderのtoStringメソッドは新しいStringを作成し、s1は内部で消滅するため、スレッドセーフです
public static String method4(){
StringBuilder s1 = new StringBuilder();
s1.append("a");
s1.append("b");
return s1.toString();
}
public static void main(String[] args) {
StringBuilder s = new StringBuilder();
new Thread(()->{
s.append("a");
s.append("b");
}).start();
method2(s);
}
}