Java仮想マシンの詳細な理解_第8章_仮想マシンのバイトコード実行エンジン

<< Java仮想マシンの詳細な理解>>に基づくメモ

概観

物理マシンの実行エンジンは、プロセッサ、キャッシュ、および命令セットのオペレーティングシステムレベルに直接構築されています

仮想マシン実行エンジンはソフトウェアによって実装され、物理的な条件に制限されることなく命令セットと実行エンジンの構造をカスタマイズでき、ハードウェアで直接サポートされていない命令セットフォーマットを実行できます。

実行エンジンがバイトコードを実行する場合、解釈、実行、およびコンパイルの2つのオプションがあります。ただし、入力と出力は同じです。入力バイトコードバイナリストリーム、処理プロセスはバイトコード分析と実行の同等のプロセス、出力は実行結果です。

ランタイムスタックフレーム構造

仮想マシンはメソッドを最も基本的な実行単位として受け取り、スタックフレームはメソッドに対応します。メソッドは、実行中の仮想マシンのデータ領域にある仮想マシンスタックのスタック要素です。呼び出しの開始から実行の終了までの各メソッドのプロセスは、スタックから仮想マシンスタック内のスタックへのスタックフレームのプロセスに対応します。

スタックフレームには、ローカル変数テーブル、オペランドスタック、動的接続、メソッドの戻りアドレス、および追加情報が格納されます

スタックフレームに割り当てる必要があるメモリの量は、プログラムのソースコードがコンパイルされ、メソッドテーブルのCode属性に書き込まれるときに計算されます。これは、プログラムの実行時に変数データの影響を受けず、プログラムのソースコードと特定の仮想にのみ依存しますスタックメモリレイアウト

Javaプログラムの観点から、同時に、同じスレッド上で、コールスタック上のすべてのメソッドが同時に実行状態にある

実行エンジンの場合、アクティブスレッドでは、スタックの最上位のメソッドのみが実行されています。これは、現在のスタックフレームと現在のメソッドと呼ばれます。

ローカル変数テーブル

一連の変数値の格納スペースであり、メソッド内で定義されたメソッドパラメータとローカル変数を格納するために使用されます

JavaプログラムがClassファイルにコンパイルされると、メソッドを割り当てる必要があるローカル変数テーブルの最大容量は、メソッドのCode属性のmax_localsデータ項目で決定されます

ローカル変数テーブルは可変スロットの最小単位であり、可変スロットは32ビット以内のデータ型を格納できます

引用するには、少なくとも2つのことを行う必要があります。

	1.  根据引用直接或间接地查找对象在Java堆中的数据存放的起始地址或索引
	2.  根据引用直接或间接地查找对象所属数据类型在方法区中的存储的类型信息

64ビットデータの場合、2つの連続する可変スロットスペース(long、double)が割り当てられ、32ビットデータの場合、スプリットビットは2回読み書きされます。ローカル変数テーブルはスレッドスタックに組み込まれ、スレッドプライベートデータに属しているため、2つの連続する変数スロットがアトミック操作であるかどうかに関係なく、データの競合やスレッドの安全性の問題は発生しません。

メソッドが呼び出されると、仮想マシンはローカル変数テーブルを使用して、パラメーター変数リストへのパラメーター値の転送プロセス、つまり、実際のパラメーターの仮パラメーターへの転送を完了します。インスタンスメソッドの場合、ローカル変数テーブルのインデックスが0の変数スロットには、メソッドがデフォルトで属するオブジェクトインスタンスへの参照が格納されます。つまり、これは

可変スロットは再利用でき、範囲外のスロットは再割り当てできます

ローカル変数テーブルには準備段階がないため、ローカル変数が定義されているが初期値が割り当てられていない場合、それを使用することはできません。コンパイラは、コンパイル中にこのポイントをチェックしてプロンプトを出すことができます。

オペランドスタック

最大深度は、コンパイル時にCode属性のmax_stacksにも書き込まれます

32ビットのデータ型によって占有されるスタック容量は1で、64は2によって占有され、オペランドスタックの深さは、max_stacksによって設定された最大値を常に超えない

算術演算を行うときは、演算に関係するオペランドをスタックの一番上にプッシュしてから、演算命令を呼び出します。

たとえば、iadd命令では、2つのint型が実行時にオペランドスタックの一番上と2番目の要素に格納されている必要があります。この命令を実行すると、2つのintがスタックからポップされて追加され、スタックに再挿入されます。

ダイナミックリンク

メソッド呼び出し中の動的接続をサポートするために、各スタックフレームには、ランタイム定数プールでスタックフレームが属するメソッドへの参照が含まれています。

メソッドの戻りアドレス

終了する2つの方法:

通常の呼び出し完了:実行エンジンが任意のメソッドから返されたバイトコード命令を検出すると、上位のメソッド呼び出し元に戻り値が渡される可能性があります

例外呼び出しが完了しました:メソッドの実行中に例外が発生し、メソッドの例外テーブルに一致する例外ハンドラーが見つかりませんでした

メソッドが終了すると、元のメソッドが呼び出されたときの位置に戻る必要があります。正常終了の場合、メインメソッドのPCカウンター値が保存されます

メソッドが終了したとき:上位メソッドのローカル変数テーブルとオペランドスタックを復元し、戻り値を呼び出し元のスタックフレームのオペランドスタックにプッシュし、メソッド呼び出し命令の後の命令を指すようにPCカウンターの値を調整します。

追加情報

デバッグやパフォーマンス収集に関する情報など、仕様に記載されていない情報をスタックフレームに追加できます。

通常、動的接続、メソッドの戻りアドレス、および追加情報は、スタックフレーム情報と呼ばれる1つのカテゴリにグループ化されます。

メソッド呼び出し

呼び出されたメソッドのバージョン(どのメソッドが呼び出されたか)を判別し、メソッド内の特定の操作プロセスが当面関与していない

Classファイルに保存されているすべてのメソッド呼び出しは、実際のランタイムメモリレイアウト(つまり、直接参照)でのメソッドのエントリアドレスではなく、シンボリック参照のみです。

したがって、一部の呼び出しは、ターゲットメソッドの直接参照を決定するために、クラスの読み込み中または実行中にも行う必要があります。

解析中

クラスの解析フェーズでは、プログラムが実際に実行される前にこれらのメソッドに特定可能なバージョンの呼び出しがあり、実行時に変更されない場合、一部のシンボル参照が直接参照に変換されます。

「コンパイル時に認識可能、実行時に不変」に準拠する場合、主に静的メソッドとプライベートファイルの2つのカテゴリがあります。前者はタイプに直接関連し、後者は外部からアクセスできません。

バイトコード命令を呼び出す

  • invokestaticは静的メソッドを呼び出すために使用されます
  • invokespecialは、()メソッド、プライベートメソッド、親クラスのメソッドを呼び出すために使用されます
  • invokevirtualは、すべての仮想メソッドを呼び出すために使用されます
  • invokeinterfaceは、インターフェースメソッドを呼び出すために使用され、インターフェースを実装するオブジェクトは実行時に決定されます
  • Invokedynamicは、最初に実行時に呼び出しサイト修飾子によって参照されるメソッドを動的に解決し、次にそれを実行します

メソッドがinvokestaticおよびinvokespecial命令によって呼び出せる限り、一意の呼び出しバージョンは解析フェーズで決定できます

静的メソッド、プライベートメソッド、インスタンスコンストラクター、親メソッド、さらにfinalによって変更されたメソッド(invokevirtualによって変更されますが)には、これらの条件を満たすメソッドの合計があります。

これらの5つのタイプのメソッドは、クラスをロードするときに、シンボリック参照をメソッドの直接参照に解決できます。まとめて非仮想メソッドと呼ばれます。

解決呼び出しは静的プロセスでなければなりません

ディスパッチ

静的ディスパッチ

static abstract class Human
{
}
static class Man extends Human
{
}
static class Woman extends Human
{
}

public void sayHello(Human guy)
{
    System.out.println("Human");
}
public void sayHello(Man guy)
{
    System.out.println("Man");
}
public void sayHello(Woman guy)
{
    System.out.println("Women");
}

public static void main(String[] args)
{
    Human man = new Man();
    Human women = new Woman();
    MainTest mainTest = new MainTest();
    mainTest.sayHello(man);
    mainTest.sayHello(women);
    /*
    result:
        Human
        Human
     */
}

Human man = new Man();では、人間は静的型と呼ばれ、人間は実際の型と呼ばれます

最終的な静的型は再コンパイル中に認識され、実際の型変更の結果は実行時にのみ決定できます

仮想マシンが過負荷になると、実際のタイプではなく、パラメーターの静的タイプが判断基準として使用されます。コンパイル段階では、コンパイラーはパラメーターの静的タイプに従って、どのオーバーロードバージョンを使用するかを決定します

メソッド実行のバージョンを決定するために静的型に依存するすべてのディスパッチアクションは、静的ディスパッチと呼ばれます。最も一般的なアプリケーションは、メソッドのオーバーロードです。

コンパイラはメソッドのオーバーロードされたバージョンを判別できますが、多くの場合、オーバーロードされたバージョンは一意ではなく、比較的適切なバージョンのみを判別できます。

動的ディスパッチ

多態性の書き換えの実現です。

実行時に変数の実際のタイプに従ってメソッド実行バージョンを配布する

フィールドは多型に参加しません

今日のJava言語は、静的マルチディスパッチ言語、動的シングルディスパッチ言語です

仮想解析プロセスを呼び出す

  1. オペランドスタックの最上部にある最初の要素が指すオブジェクトの実際のタイプを検索します(Cと表記)
  2. 定数の記述子と単純名に一致するメソッドがタイプCで見つかった場合、アクセス許可の検証が実行され、成功した場合、このメソッドの直接参照が返され、検索プロセスが終了します。失敗するとIllegalAccessErrorが返されます
  3. それ以外の場合は、継承関係に従って、Cの各親クラスに対して下から上へ、検索および検証プロセスの2番目のステップが実行されます。
  4. 適切なメソッドが見つからない場合、AbstractMethodErrorがスローされます

仮想マシンの動的ディスパッチの実現

一般的な最適化方法は、メソッド領域に仮想メソッドテーブル(vtable)を作成し、メタデータの代わりに仮想メソッドテーブルインデックスを使用して、検索パフォーマンスを向上させることです。

仮想メソッドテーブルは、各メソッドの実際のエントリアドレスを格納します。サブクラスでメソッドがオーバーライドされていない場合、サブクラスの仮想メソッドテーブルのアドレスエントリは、親クラスの同じメソッドのアドレスエントリと同じであり、それらすべてがポイントします。親クラスの実現入り口。

サブクラスがオーバーライドされると、サブクラスの実装バージョンを指すエントリアドレスに置き換えられます。

仮想メソッドテーブルは、通常、クラスの読み込みの接続フェーズで初期化されますが、クラス変数の初期値を準備した後、仮想マシンもクラスの仮想メソッドテーブルを初期化します。

最終変更のないデフォルトのメソッドは仮想メソッドです

動的に型付けされた言語のサポート

動的型言語のサポートを実現するために生成されたInvokedynamic命令

動的に型付けされた言語

主な機能は次のとおりです。型チェックのメインプロセスは、コンパイル時ではなく実行時に実行されます。

実行時例外:コードがこの行まで実行されない限り、例外は生成されません

接続中の例外:コードがまったく実行できないパスブランチに配置されている場合でも、クラスが読み込まれると例外がスローされます

動的型付け言語のもう1つのコア機能:変数には型がなく、変数値には型があります

長所と短所

  1. 静的に型付けされた言語は、コンパイル中に変数の型を判別でき、コンパイラーは包括的かつ厳密な型チェックを改善できます。これにより、安定性が向上し、プロジェクトがより大規模に到達しやすくなります
  2. タイプは動的に型付けされた言語の実行時にのみ決定されます。これにより、開発者は優れた柔軟性と明確性を得ることができ、開発効率が向上します。

java.lang.invoke包

MethodHandleとReflectionの違い

  • リフレクションはJavaコードレベルでシミュレートされるメソッド呼び出しであり、MethodHandleはバイトコードレベルをシミュレートするメソッド呼び出しです
  • リフレクションはヘビー級であるJava側の包括的なイメージであり、MethodHandleは軽量です
  • リフレクションは最適化が困難であり、MethodHandleはさまざまな最適化(メソッドのインライン化など)を実現できます。
  • ReflectionはJava言語のみを提供し、MethodHandleはすべてのJava仮想マシン言語を提供するように設計されています

スタックベースのバイトコード解釈実行エンジン

メソッドでバイトコード命令を実行する方法について説明します。解釈と実行、およびコンパイルと実行の2つのタイプがあります。

実行について説明する

IMG_20200825_184807

実行前に、プログラムのソースコードに対して字句解析と構文解析が実行され、ソースコードが抽象ディレクトリツリーに変換されます。

スタックベースの命令セットとレジスタベースの命令セット

Javacコンパイラが出力するバイトコード命令ストリームは、基本的にスタックベースの命令セットアーキテクチャです。バイトコード命令ストリームのほとんどはゼロアドレス命令であり、作業はオペランドスタックに依存しています。

たとえば、1 + 1:

iconst_1
iconst_1
iadd
istore_0

2つのiconst_1命令が2つの定数1を連続してスタックにプッシュした後、iadd命令は2つの値をスタックの最上部にポップし、それらを追加して、結果をスタックの最上部に戻し、最後にistore_0がスタックの最上部にある値をローカル変数テーブルに入れます0番目の可変スロット

長所と短所

  • スタックベースの主な利点は、移植可能であることです。スタックアーキテクチャを使用すると、ユーザープログラムはレジスタを直接使用しません。仮想マシンで実装して、最も頻繁にアクセスされるデータ(プログラムカウンター、スタックトップキャッシュなど)をレジスターに入れて、パフォーマンスを向上させることができます。 。コードはコンパクトで、コンパイラーの実装は簡単です
iconst_1
iconst_1
iadd
istore_0

2つのiconst_1命令が2つの定数1を連続してスタックにプッシュした後、iadd命令は2つの値をスタックの最上部にポップし、それらを追加して、結果をスタックの最上部に戻し、最後にistore_0がスタックの最上部にある値をローカル変数テーブルに入れます0番目の可変スロット

長所と短所

  • スタックベースの主な利点は、移植可能であることです。スタックアーキテクチャを使用すると、ユーザープログラムはレジスタを直接使用しません。仮想マシンで実装して、最も頻繁にアクセスされるデータ(プログラムカウンター、スタックトップキャッシュなど)をレジスターに入れて、パフォーマンスを向上させることができます。 。コードはコンパクトで、コンパイラーの実装は簡単です
  • 欠点は、実行速度がわずかに遅くなること、同じ関数を完了するために必要な命令の数が多いこと、そして頻繁なスタックアクセスが頻繁なメモリアクセスを意味することです。

おすすめ

転載: blog.csdn.net/weixin_42249196/article/details/108253734