JVM仮想マシン5の詳細な理解:仮想マシンのバイトコード実行エンジン

1。概要

実行エンジンは、Java仮想マシンのコアコンポーネントの1つです。仮想マシンの実行エンジンはそれ自体で実装されるため、命令セットと実行エンジンの構造をカスタマイズしたり、ハードウェアで直接サポートされていない命令セット形式を実行したりできます。

すべてのJava仮想マシンの実行エンジンは同じです。入力はバイトコードファイルであり、処理プロセスはバイトコード解析と同等のプロセスであり、出力は実行結果です。このセクションでは、主に概念モデルの観点から仮想マシンのメソッド呼び出しとバイトコード実行について説明ます。

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

スタックフレーム は、仮想マシンのメソッド呼び出しとメソッドの実行をサポートするために使用されるデータ構造です。これは、仮想マシンランタイムのデータ領域にある仮想マシンスタック(仮​​想マシンスタック)のスタック要素です

スタックフレームには、メソッドのローカル変数テーブル、オペランドスタック、動的接続、メソッドの戻りアドレス、およびその他の情報が格納されます。呼び出しの開始から実行の完了までの各メソッドのプロセスは、スタックから仮想マシンスタック内のスタックへのスタックフレームのプロセスに対応します。

スタックフレームの概念構造を次の図に示します。

スタックフレームの概念構造

2.1ローカル変数テーブル

ローカル変数テーブルは、メソッドパラメータとメソッドで定義されたローカル変数を格納するために使用される変数値ストレージスペースのグループです。
ローカル変数テーブルの容量は、最小単位として可変スロットを使用します。
 スロットは、32ビット以内のデータ型(boolean、byte、char、short、int、float、reference、およびreturnAddress)を格納できます。参照型はオブジェクトインスタンスへの参照を表します。ReturnAddressはまれであり、無視できます。

64ビットデータ型(Java言語で明確に定義されている64ビットデータ型はlongとdoubleのみ)の場合、仮想マシンは2つの連続するスロットスペースを高整列で割り当てます。

仮想マシンは、インデックスの配置によってローカル変数テーブルを使用し、インデックス値の範囲は0からローカル変数テーブルのスロットの最大数までです。アクセスされる変数は32ビットデータ型です。インデックスnはn番目のスロットの使用を表します。64ビットデータ型の場合、スロットnとn +1の両方が同時に使用されることを意味します。

スタックフレームスペースを節約するために、ローカル変数Slotを再利用できます。メソッド本体で定義された変数のスコープは、必ずしもメソッド本体全体をカバーしているわけではありません。現在のバイトコードPCカウンタ値が特定の変数のスコープを超える場合、この変数のスロットを他の変数で使用できます。このような設計は、次のようないくつかの追加の副作用をもたらします。場合によっては、スロットの再利用がシステムの収集動作に直接影響します。

2.2オペランドスタック

オペランドスタック(オペランドスタック) も、多くの場合、それは、操作スタックと呼ばれる後入れ先出しスタックメソッドの実行が開始されると、このメソッドのオペランドスタックは空になります。メソッドの実行中に、オペランドスタックからコンテンツを書き込んだり抽出したりするためのさまざまなバイトコード命令、つまりポップ/プル 操作があります。

オペランドスタック

概念モデルでは、アクティブなスレッド内の2つのスタックフレームは互いに独立しています。ただし、ほとんどの仮想マシンの実装では、いくつかの最適化が行われます。次のスタックフレームのオペランドスタックの一部を前のスタックフレームのローカル変数テーブルの一部とオーバーラップさせます。これの利点は、データの一部を次の場合に共有できることです。メソッドが呼び出され、追加のパラメータコピー転送を実行する必要はありません。

2.3動的接続

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

バイトコードのメソッド呼び出し命令は、定数プール内のメソッドへのシンボル参照をパラメーターとして受け取ります。一部のシンボル参照は、クラスのロード段階または最初に使用されるときに直接参照に変換されます。この変換は静的解決と呼ばれます。  、他の部分は各実行中に直接参照に変換されますこの部分は動的接続と呼ばれます

2.4メソッドの戻りアドレス

メソッドが実行されると、メソッドを終了する方法は2つあります。

  • 1つ目は、実行エンジンが任意のメソッドから返されたバイトコード命令に遭遇することです。この終了メソッドは、通常のメソッド呼び出し完了と呼ばれます。

  • もう1つは、メソッドの実行中に例外が発生し、その例外がメソッド本体で処理されない(つまり、このメソッドの例外処理テーブルに一致する例外ハンドラーがない)ため、メソッドが発生することです。このexitメソッドは、Abrupt Method Invocation Completion(Abrupt Method Invocation Completion)と呼ばれます。
    注:このexitメソッドは、上位の呼び出し元への戻り値を生成しません。

どのexitメソッドを使用しても、メソッドが終了した後、プログラムの実行を続行するには、メソッドが呼び出された場所に戻る必要があります。メソッドが戻ったときに、スタックフレームに情報を保存する必要がある場合があります。その上位メソッドの実行を復元するのに役立ちます。一般的に、メソッドが正常に終了すると、呼び出し元のPCカウンターの値をリターンアドレスとして使用でき、このカウンター値はスタックフレームに保存される可能性があります。メソッドが異常終了した場合、戻りアドレスは例外ハンドラテーブルによって決定され、情報のこの部分は通常、スタックフレームに保存されません。

メソッドexitのプロセスは、実際には現在のスタックフレームをポップするのと同じです。したがって、終了時に実行できる操作は、ローカル変数テーブルと上位メソッドのオペランドスタックを復元し、戻り値(存在する場合)をにプッシュすることです。呼び出し元のスタックフレームのオペランドスタックで、メソッド呼び出し命令などの後の命令を指すようにPCカウンターの値を調整します。

2.5追加情報

仮想マシンの仕様により、仮想マシンの実装では、デバッグ関連の情報など、カスタムの追加情報をスタックフレームに追加できます。

3メソッド呼び出し

メソッド呼び出しフェーズの目的:呼び出されたメソッドのバージョン(どのメソッド)判別するために、メソッド内の特定の操作プロセスは含まれません。プログラムの実行中は、メソッド呼び出しが最も一般的で頻繁な操作です。

クラスファイルに格納されているすべてのメソッド呼び出しはシンボル参照のみであり、クラスのロード中または実行時に実際のランタイムメモリレイアウトでメソッドのエントリアドレスとして決定する必要があります(前述の直接参照と同等)

3.1分析

「コンパイル時に認識し、実行時に不変」であるメソッド(静的メソッドとプライベートメソッド)は、クラスの読み込みの解析フェーズで、シンボリック参照を直接参照(エントリアドレス)に変換します。このタイプのメソッドの呼び出しは「解決と呼ばれます。

5は、Java仮想マシンは、バイトコード命令を呼び出し内のメソッドを提供します:
invokestatic  :静的メソッドの呼び出し
invokespecial:コンストラクタメソッドの呼び出しインスタンス、プライベートメソッドは、メソッド継承
INVOKEVIRTUAL:すべての仮想メソッド呼び出し
invokeinterface:メソッドインタフェースの呼び出し、オブジェクトを実装するこのインタフェースは、実行時に決定されます
invokedynamicの:ドット修飾子が参照する方法は、実行時に動的に解析され、その後、メソッドが実行される前の4つの呼び出しコマンドのディスパッチロジックは、これは、Java仮想マシンで固化されます。 、invokedynamic命令のディスパッチロジックは、ユーザーが設定したガイダンスメソッドによって決定されます。

3.2発送

ディスパッチ呼び出しプロセスは、「オーバーロード」と「上書き」がJava仮想でどのように実装されるかなど、ポリモーフィズムの最も基本的な兆候のいくつかを明らかにします。

1静的ディスパッチ

メソッドの実行バージョンを見つけるために静的型に依存するすべてのディスパッチアクションは、静的ディスパッチと呼ばれます。静的ディスパッチは、コンパイルフェーズ中に発生します。

静的ディスパッチの最も一般的なアプリケーションは、メソッドのオーバーロードです。

package jvm8_3_2;

public class StaticDispatch {
    static abstract class Human {

    }

    static class Man extends Human {

    }

    static class Woman extends Human {

    }

    public void sayhello(Human guy) {
        System.out.println("Human guy");

    }

    public void sayhello(Man guy) {
        System.out.println("Man guy");

    }

    public void sayhello(Woman guy) {
        System.out.println("Woman guy");
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        StaticDispatch staticDispatch = new StaticDispatch();
        staticDispatch.sayhello(man);// Human guy
        staticDispatch.sayhello(woman);// Human guy
    }

}

演算結果:

人間の男

人間の男

なぜそのような結果があるのですか?

Human man = new Man();その中で、Humanは変数の静的型(Static Type)と呼ばれ、Manは変数の実際の型(Actual Type)と呼ばれます
2つの違いは次のとおりです。静的型はコンパイラによって認識され、実際の型は実行時まで決定されません。
オーバーロード時には、実際のタイプではなく、パラメーターの静的タイプが判断基準として使用されます。したがって、コンパイルフェーズでは、Javacコンパイラーが、パラメーターの静的タイプに従って、使用するオーバーロードバージョンを決定します。したがって、呼び出し対象としてsayhello(Human)を選択し、このメソッドのシンボリック参照をmain()メソッドの2つのinvokevirtual命令のパラメーターに書き込みます。

2動的ディスパッチ

実行時に実際のタイプに従ってメソッドの実行バージョンを決定するディスパッチプロセスは、動的ディスパッチと呼ばれます。最も一般的なアプリケーションはメソッドの書き換えです。

package jvm8_3_2;

public class DynamicDisptch {

    static abstract class Human {
        abstract void sayhello();
    }

    static class Man extends Human {

        @Override
        void sayhello() {
            System.out.println("man");
        }

    }

    static class Woman extends Human {

        @Override
        void sayhello() {
            System.out.println("woman");
        }

    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        man.sayhello();
        woman.sayhello();
        man = new Woman();
        man.sayhello();
    }

}

演算結果:

おとこ

女性

女性

3シングルディスパッチとマルチディスパッチ

メソッドの受信者とメソッドのパラメーターは、メソッドの数量と呼ぶことができます。バッチが基づいている数量の種類に応じて、配布は単一の配布と複数の配布に分けることができます。単一配布では、1つの数量に基づいてターゲットメソッドが選択され、複数配布では、複数の数量に基づいてターゲットメソッドが選択されます。

Javaが静的ディスパッチを実行する場合、ターゲットメソッドは2つのポイントに基づいて選択する必要があります。1つは変数の静的タイプであり、もう1つはメソッドパラメータのタイプです。選択は2つの変数に基づいているため、Java言語の静的ディスパッチは複数配布タイプに属します。

実行時の動的ディスパッチプロセスでは、コンパイラがターゲットメソッドのシグネチャ(メソッドパラメータを含む)を決定するため、ランタイム仮想マシンはディスパッチする前にメソッドの受信者の実際のタイプを決定するだけで済みます。選択の基準として数量に基づいているため、Java言語の動的ディスパッチは単一のディスパッチタイプに属します。

注:JDK1.7の時点では、Java言語は依然として静的マルチディスパッチおよび動的シングルディスパッチ言語であり、将来的には動的マルチディスパッチをサポートする可能性があります。

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

動的ディスパッチは非常に頻繁なアクションであり、動的ディスパッチはメソッドバージョンの選択プロセス中にメソッドメタデータで適切なターゲットメソッドを検索する必要があるため、仮想マシンの実装は通常、パフォーマンスを考慮してそのような頻繁な検索を直接実行しません。最適化方法。

「安定した最適化」メソッドの1つは、クラスのメソッド領域に仮想メソッドテーブル(仮想メソッドテーブル、vtableとも呼ばれます)を作成することですこれに対応して、インターフェイスメソッドテーブル-インターフェイスメソッドテーブルもありますitableとして知られています。パフォーマンスを向上させるには、メタデータルックアップの代わりに仮想メソッドテーブルインデックスを使用します。原理はC ++の仮想関数テーブルに似ています。

仮想メソッドテーブルには、各メソッドの実際のエントリアドレスが格納されます。サブクラスでメソッドがオーバーライドされていない場合、サブクラスの仮想メソッドテーブルのアドレスエントリは親クラスのメソッドと同じであり、どちらも親クラスの実装エントリを指します。仮想メソッドテーブルは通常、クラスロードの接続フェーズで初期化されます。

3.3動的型付け言語のサポート

JDKは、「動的型言語」を実現するために、invokedynamic命令を新たに追加しました。

静的言語と動的言語の違い:

  • 静的言語(強く型付けされた言語)
    静的言語は、コンパイル時に変数のデータ型を決定できる言語です。ほとんどの静的型付け言語では、変数を使用する前にデータ型を宣言する必要があります。 
    例:C ++、Java、Delphi、C#など。
  • 動的言語(弱い型の言語)  :
    動的言語は、実行時にデータ型を決定する言語です。変数を使用する前に型宣言は必要ありません。通常、変数の型は割り当てられている値の型です。 
    たとえば、PHP / ASP / Ruby / Python / Perl / ABAP / SQL / JavaScript / Unixシェルなどです。
  • 強く型付けされた定義言語 :
    データ型の定義強制する言語つまり、変数に特定のデータ型が割り当てられると、それが強制されない場合、その変数は常にこのデータ型になります。例:整数変数aを定義した場合、プログラムがaを文字列型として扱うことはできません。強く型付けされた言語は型安全言語です。
  • 弱い型の定義言語 :
    データ型を無視できる言語強く型付けされた定義言語とは対照的に、変数にはさまざまなデータ型の値を割り当てることができます。強く型付けされた言語は、速度の点で弱い型付けの言語よりもわずかに劣る可能性がありますが、強く型付けされた言語によってもたらされる厳密さは、多くのエラーを効果的に回避できます。

4スタックベースのバイトコードインタプリタ実行エンジン

仮想マシンがメソッドを呼び出す方法の内容について説明したので、次に、仮想マシンがメソッド内のバイトコード命令を実行する方法について説明します。

4.1解釈と実行

Java言語は、「解釈された実行」言語として位置付けられることがよくあり ます。Javaが誕生したJDK1.0の時代では、この定義はまだ比較的正確でした。ただし、主流の仮想マシンにインスタントコンパイルが含まれている場合、クラスファイルのコードは次のようになります。結局、それが解釈されるのか、実行されるのか、コンパイルされて実行されるのかは、仮想マシンだけが正確に判断できるものです。その後、Javaはネイティブコードを直接生成するコンパイラも開発し[How to GCJ(Java用GNUコンパイラ)]、C / C ++もインタプリタバージョン(CINTなど)を通じて登場し、一般的に「解釈された実行」と言うようになりました。 Java言語全体にとってほとんど意味のない概念。議論の対象が特定のJava実装バージョンと実行エンジンの動作モードであると判断された場合にのみ、インタプリタ実行またはコンパイル実行について話すことができますか?より正確に

解釈と実行

Java言語では、javacコンパイラは、プログラムコードの語彙分析と文法分析のプロセスを完了して抽象構文木にした後、構文ツリーをトラバースして線形バイトコード命令ストリームを生成します。これは、アクションのこの部分が外部で実行されるためです。 Java仮想マシン、およびインタープリターは仮想マシン内にあるため、Javaプログラムのコンパイルは半独立して実装されます。

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

Javaコンパイラによって出力される命令ストリームは、基本的にスタックベースの命令セットアーキテクチャ(命令セットアーキテクチャ、ISA)であり、作業のためにオペランドスタックに依存します同様に、別の一般的に使用される命令セットアーキテクチャは、レジスタベースの命令セットであり 、レジスタに依存して動作します。

では、スタックベースの命令セットとレジスタベースの命令セットの違いは何ですか?

簡単な例として、次の2つの命令を使用して、1 + 1の結果を計算しますスタックベースの命令セットは、次のようになります
。iconst_1

iconst_1

私は追加します

istore_0

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

命令セットがレジスタに基づいている場合、プログラムは次のようになります。

mov eax、1

eaxを追加、1

mov命令はEAXレジスタの値を1に設定し、次にadd命令は値に1を加算し、結果はEAXレジスタに格納されます。

スタックベースの命令セットの主な利点は、移植性があることです。レジスタはハードウェアによって直接提供され、プログラムはこれらのハードウェアレジスタに直接依存するため、必然的にハードウェアの制約を受けます。

スタックアーキテクチャの命令セットには、比較的コンパクトなコードやコンパイラの実装が簡単になるなど、他にもいくつかの利点があります。
スタックアーキテクチャ命令セットの主な欠点は、実行速度が比較的遅いことです。

総括する

このセクションでは、仮想マシンがコードを実行するときに正しいメソッドを見つける方法、メソッド内のバイトコードを実行する方法、およびコードの実行に関連するメモリ構造を分析します。

おすすめ

転載: blog.csdn.net/wr_java/article/details/115209048