「Java仮想マシンの詳細な理解」リーディングノート(7)-仮想マシンのバイトコード実行エンジン(オン)

目次

 

序文

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

1.1ローカル変数テーブル

1.2オペランドスタック

1.3動的接続

1.4メソッドのリターンアドレス

1.5追加情報

次に、実行方法を決定します

2.1分析

2.2発送

2.2.1静的ディスパッチ

2.2.2動的ディスパッチ

2.2.3単一ディスパッチと複数ディスパッチ

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


序文

この章では、主に、仮想マシンが呼び出されるメソッドのバージョンを決定する方法と、メソッドを実行する方法について説明します。

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

1.1ローカル変数テーブル

メソッドで定義されたメソッドパラメータローカル変数を格納するために使用されます。コンパイル段階では、メソッドテーブルのCode属性のmax_localsデータ項目によって、メソッドに必要なローカル変数テーブルの最大スペースが決まります。その容量は、最小単位としての可変スロット(slot)に基づいています。仮想マシンの仕様では、スロットが占めるスペースのサイズは明確に指定されていませんが、各スロットは、 boolean、byte、char、short、int、float、reference、またはreturnAddressタイプのデータ、これらの8つのデータタイプは32ビット以下のメモリに格納できますが、スロットの長さもプロセッサによって異なり、動作します。システムまたは仮想マシン、ただし、スロットの実装に64ビットのメモリスペースが使用されている場合でも、スロットの外観を32ビットの仮想マシンと一致させるために、仮想マシンは位置合わせとパディングを使用する必要があります。 。

注:参照タイプに関して、仮想マシンの仕様では、32ビットまたは64ビットを占める可能性のある長さを明確に指定しておらず、どのような構造にする必要があるかを明確に示していません。一般的に、仮想マシンは次のことができる必要があります。少なくともこのリファレンスを通じて。2つのポイント:

  • このリファレンスから、Javaヒープ内のオブジェクトのデータストレージの開始アドレスインデックスを直接または間接的に見つけることができます。

  • このリファレンスから、オブジェクトのデータ型のメソッド領域に格納されている型情報を直接または間接的に見つけることができます

Java言語で定義されている64ビットデータ型には、longとdoubleの2種類しかありません。64ビットデータ型の場合、仮想マシンは2つの連続するスロットスペースを高整列で割り当てます。ローカル変数テーブルが作成されるためです。スレッドのプライベートスタックスペースでは、2つの連続するスロットの読み取りと書き込みがアトミック操作であるかどうかに関係なく、データセキュリティの問題は発生しません。これは、セキュリティの問題を引き起こす可能性があるlongおよびdoubleの非アトミックプロトコルとは異なります。 。

仮想マシンは、インデックスの配置によってローカル変数テーブルを使用します。インデックスの範囲は、0からローカル変数テーブルのスロットの最大数までです。32ビットデータにアクセスしている場合、インデックスnはn番目のスロットを表します。64ビットデータにアクセスしている場合、スロットnとn +1の両方が同時に使用されることを意味します。64ビットデータを一緒に格納する2つの隣接するスロットの場合、いずれかの方法を使用してそれらの1つに個別にアクセスすることは許可されていません。

注:非静的メソッドの場合、ローカル変数テーブルのインデックスが0番目のスロットは、メソッドが属するオブジェクトインスタンスの参照を渡すためにデフォルトで使用されるため、「this」を介して暗黙的なパラメーターにアクセスできます。メソッド内のキーワード。

さらに、スタックフレームスペースを節約するために、スロットを再利用できます。プログラムカウンタの値が変数のスコープを超えた場合、この変数に対応するスロットを他の変数で使用できます。

しかし、概念モデルから、スロットの再利用はGCの問題を引き起こす可能性があります:ローカル変数が大きなオブジェクトを参照し、変数がそのスコープを超えています。現時点では大きなオブジェクトが役に立たないのは当然であり、GCはそれを取り戻すことができますただし、スロットの再利用により、スロットが再利用されていない場合でも、GCルートとして大きなオブジェクトへの参照が維持されるため、GCはそれを再利用できません。コードビハインドで時間のかかる操作がある場合、フロントで占有される大きなオブジェクトは大きな負担になるため、変数を手動でnull値に設定するための「推奨事項」が徐々にあります。しかし、作者が意味するのは、この操作はバイトコード実行エンジンの概念モデルの理解にのみ基づいているということです。仮想マシンがインタープリターを使用して実行される場合、通常は概念モデルに近いですが、JITコンパイル後は仮想マシンがコードを実行する主な方法は、JITのコンパイルと最適化の後でヌル値を割り当てる操作がなくなり、JITのコンパイル後、オブジェクトがスコープを超えて参照されると、GCは通常正常に回復できるためです。この「サオ操作」に頼る必要はありません。

ローカル変数はクラス変数とは異なります。ローカル変数は初期値なしで定義されている場合は使用できません。割り当てられていないローカル変数が使用されている場合、コンパイラはコンパイル中にエラーを報告します。バイトコードが手動で生成されている場合、コンパイラはスキップされます。チェックは、クラスロードのバイトコード検証フェーズでも検出されます。

1.2オペランドスタック

通常のスタックデータ構造であるFILOと同様に、その最大深度はコンパイル時メソッドのCode属性のmax_statcks項目に書き込まれ、後で変更されることはありません。メソッドの実行中、さまざまなバイトコード命令がオペランドスタックに継続的にプッシュ/ポップされます。スタック内のデータ要素は、バイトコード命令のシーケンスと厳密に一致する必要があります。これは、コンパイラのコンパイル時に確認する必要があり、クラス検証フェーズで(StackMapTableを介して再度検証する必要がありますたとえば、iaddを使用して整数データの加算を実行する場合、スタックの最上位にある2つの要素はint型である必要があります。

さらに、概念モデルでは、2つのスタックフレームは互いに独立していますが、ほとんどの仮想マシンの実装では、2つのスタックフレームをオーバーラップさせるためにいくつかの最適化が行われます。下のスタックフレームと上のスタックフレームのオペランドスタックを許可します。スタックフレームのローカル変数テーブルは重複しているため、メソッドが呼び出されたときにデータの一部を共有でき、追加のパラメーターの割り当てや転送は必要ありません。

1.3動的接続

クラスファイルの定数プールには多数のシンボル参照があり、バイトコードのメソッド呼び出し命令は、定数プールのメソッドを指すシンボル参照をパラメーターとして受け取ります。これらのシンボル参照の一部は、クラスのロード段階または初めて使用されるときに直接参照に直接変換されます。この変換は静的解像度と呼ばれます他の部分は、実行ごとに直接参照に変換されます。この部分はダイナミックリンクと呼ばれます。

1.4メソッドのリターンアドレス

終了するには2つの方法があります。

  • 1つ目は、実行エンジンが返されたバイトコード命令を検出することです。戻り値があるかどうか、および戻り値のタイプは、検出されたメソッドreturn命令に従って決定されます。このexitメソッドは、exitの通常の完了です
  • 他の例外は、メソッドの実行中に遭遇され、例外がメソッド本体(例外ハンドラが、この方法の例外テーブルに一致していない)で処理されていないことである。この方法は、出口完了のために例外をと生成しません。戻り値。

どのように終了しても、メソッドが終了した後、プログラムを続行するには、メソッドが呼び出された場所に戻る必要があります。一般的に、メソッドが正常に終了する場合、メソッド呼び出し元のプログラムカウンタの値をリターンアドレスとして使用し、メソッドに対応するスタックフレームに格納できます。メソッドが異常終了する場合は、例外処理テーブル。

1.5追加情報

仮想マシンの仕様により、特定の仮想マシンの実装では、仮想マシン自体によって実装されるデバッグ関連の情報など、仕様に記載されていない情報をスタックフレームに追加できます。

次に、実行方法を決定します

2.1分析

クラスファイルのコンパイルプロセスには、従来のコンパイルのリンク手順は含まれていません。すべてのメソッド呼び出しは、実際のランタイムメモリ内のメソッドのエントリアドレス(直接参照)ではなく、シンボル参照によってのみクラスファイルに格納されます。

一部のシンボル参照クラスの読み込みの解析段階で直接参照変換されます。この解析の前提は、プログラムの実行前にメソッドのバージョンが判別可能であり、実行時に変更できないことです。この前提を満たすメソッドには、主に静的メソッドプライベートメソッドが含まれますこれらの2つのメソッドは、継承または他のメソッドを介して他のバージョンをオーバーライドできないため、クラスのロード段階での解析に適しています。

Java仮想マシンは、5つのメソッド呼び出しバイトコード命令を提供します。

  • invokestatic:静的メソッドを呼び出す

  • invokespecial:インスタンスコンストラクターの<init>メソッド、プライベートメソッド、およびスーパークラスメソッドを呼び出します(super.method(...))

  • invokevirtual:すべての仮想メソッドを呼び出す

  • invokeinterface:インターフェースメソッドを呼び出します。このインターフェースを実装するオブジェクトは実行時に決定されます。

  • invokedynamic:最初に、実行時に呼び出しサイト修飾子によって参照されるメソッドを動的に解析し、次にメソッドを実行します

その中で、invokestaticおよびinvokespecialによって呼び出されるメソッドは、クラスのロードの解析段階でシンボル参照をメソッドの直接参照に解決できます。これらのメソッドには、静的メソッド、プライベートメソッド、親メソッド、および<init>メソッドが含まれます。これらnonと呼ばれます。-仮想メソッドメソッド他のメソッドは仮想メソッドと呼ばれます。

注意:

1.最終的に変更されたメソッドはinvokevirtual命令で呼び出されますが、上書きできないため、非仮想メソッドである他のバージョンはありません。

2.親クラスメソッドが非仮想メソッドである場合、invokespecial呼び出しの使用は、superを介して親クラスメソッドを呼び出す場合を指します。サブクラスがスーパークラスメソッドをオーバーライドする場合、サブクラスのメソッドはそれ自体に属します。親クラスのメソッドは大丈夫です

2.2発送

実行メソッドのバージョンを決定するための上記の解析プロセスに加えて、仮想メソッドを決定するための別のメソッド、dispatchがあります。分布は静的分布動的分布に分けられ、分布基準の量に応じて単一分布複数分布に分けることができますペアワイズの組み合わせは、静的シングルディスパッチ、静的マルチディスパッチ、動的シングルディスパッチ、および動的マルチディスパッチを構成します。

2.2.1静的ディスパッチ

静的ディスパッチの一般的なアプリケーションは、メソッドのオーバーロードを処理することです。英語の技術文書は「メソッドオーバーロードの解決と呼ばれます(本の説明では、国内の資料では一般にこの動作が「静的ディスパッチ」に変換されます)。オブジェクトAがオブジェクトBを継承する場合、ステートメントの場合:B b = new A();ここで、Bはb変数の静的型(静的型)と呼ばれ、Aはb変数の実際の型(実際の型と呼ばれます。メソッドパラメータの静的タイプは変更できます。たとえば、強制変換操作によって、bの静的タイプはBですが、(A)bの静的タイプはAに変換されます。ただし、変数自体の静的型(B)は変更されず、最終的な静的型はコンパイル時に認識されます。実際の型は実行時にのみ決定でき、コンパイラはでオブジェクトの実際の型を認識しません。コンパイル時それはなんですか。

仮想マシンは、オーバーロードを処理する際の判断の基礎として、実際の型ではなく静的型のパラメーターを使用します。前述のように、静的型はコンパイル時に認識されます。したがって、コンパイルフェーズでは、コンパイラはパラメータの静的タイプに応じて使用するオーバーロードバージョンを決定します。メソッドのオーバーロードバージョンを選択した後、コンパイラはこのメソッドのシンボリック参照をメソッドのパラメータに書き込みます。バイトコード命令を呼び出します。たとえば、次のサンプルコードには、say()メソッドのオーバーロードされたバージョンが3つあります(ここでのメインオブジェクトは一意に決定されることに注意してください)。

public class Main {

    static class A {
    }

    static class B extends A {
    }

    static class C extends B {
    }

    public void say(A a) {
        System.out.println("A");
    }

    public void say(B b) {
        System.out.println("B");
    }

    public void say(C c) {
        System.out.println("C");
    }

    public static void main(String[] args) throws Exception {
        Main main = new Main();
        B os = new C();
        main.say(os);//静态类型为B,实际类型为C,确定的say方法重载版本为say(B b)
        main.say((A) os);//最终静态类型转换为了A,实际类型为C,确定的say方法重载版本为say(A a)
        main.say((C) os);//最终静态类型转换为了C,实际类型为C,确定的say方法重载版本为say(C c)
        //输出 B A C
    }
}

さらに、コンパイラはメソッドのオーバーロードされたバージョンを判別できますが、多くの場合、このオーバーロードされたバージョンは「一意」ではなく、「より適切な」バージョンしか判別できません。このあいまいな結論の主な理由は、リテラルを定義する必要がないため、リテラルの静的型は表示されず、その静的型は言語の規則によってのみ理解および推測できるためです。

たとえば、メソッドsay(...)の場合、7つのオーバーロードされたバージョンがあります:say(char arg)、say(int arg)、say(long arg)、say(Character arg)、say(Serializable arg)、 say(Object arg)、say(char ... arg)。プログラムがメソッドを呼び出そうとした場合:say( 'a'); 'a'を定義する必要はなく、直接使用できるため、静的型は表示されません。コンパイラはどのオーバーロードバージョンを選択する必要がありますか?

  • 'a'は最初のchar型です:say(char arg)に対応します

  • 次に、97という数字を表すこともできます(ASCIIコードを参照):say(int arg)に対応します

  • 97に変換された後、long型97Lに変換することもできます:say(long arg)に対応

  • さらに、say(Character arg)に対応するCharacter:として自動的にボックス化およびパッケージ化できます。

  • ボクシングクラスのCharacterは、Serializableインターフェイスも実装します(複数のインターフェイスが直接または間接的に実装されている場合、優先度は同じです。複数のインターフェイスに適応できるオーバーロードされたメソッドが複数ある場合、タイプがあいまいになり、コンパイルを拒否します。 ):「(Serializable)」に対応

  • また、CharacterはObjectから継承します(複数の親クラスがある場合、継承関係で下から上に検索します。上位レベルに近いほど、優先度は低くなります):say(Object arg)に対応します。

  • 最終的には、可変長タイプと一致することもあります。say(char ... arg)に対応します。

上記で説明したのは、実際にはコンパイル中に静的ディスパッチターゲットを選択するプロセスです。このプロセスは、メソッドのオーバーロードのJava言語実装の本質でもあります。

注:解決と割り当ては代替の関係ではありません。これらは、さまざまなレベルでターゲットメソッドをスクリーニングおよび決定するプロセスです。たとえば、静的メソッドはクラス読み込みの解析段階で直接参照され、静的メソッドはオーバーロードされたバージョンを持つこともできます。オーバーロードされたバージョンを選択するプロセスも静的ディスパッチを通じて行われます。

2.2.2動的ディスパッチ

動的ディスパッチの典型的なアプリケーションは、メソッドの書き換えです。メソッドの書き換えの場合、Java仮想マシンは、メソッドを呼び出すときに、実際の型を介してメソッド実行バージョンをディスパッチします。次のコードの場合:

public class Main {

    static class A {
        public void say() {
            System.out.println("A");
        }
    }

    static class B extends A {
        public void say() {
            System.out.println("B");
        }
    }

    static class C extends A {
        public void say() {
            System.out.println("C");
        }
    }

    public static void main(String[] args) throws Exception {
        A b = new B();
        A c = new C();
        b.say();
        c.say();
        //输出  B C
    }
}

b.say()およびc.say()呼び出しがコンパイラーによってコンパイルされた後、メソッド呼び出しバイトコード命令(ここではinvokevirtual)と命令のパラメーター(A.say()のシンボル参照)は同じです。しかし、最終的な実行目標は同じではありません(1つのB、1つのC)。これには、invokevirtual命令の多態的な検索プロセスが含まれます。

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

b.say();ステートメントの実行プロセスは、最初にbインスタンスオブジェクトをスタックの最上位にプッシュし、次にそれをinvokevirtual命令を介して呼び出すことです。このbオブジェクトは say()メソッドのレシーバーと呼ばれます上記の手順からわかるように、最初の手順は、実行時にsayメソッドをBとして実行するレシーバーの実際のタイプを判別することです。c.say();文は同じです。したがって、2つの呼び出しのA.say()シンボル参照は、異なる直接参照に解決されます。このプロセスは、Javaメソッドの書き換えの本質です。実行時に実際の型に応じてメソッドの実行バージョンが決定されるこの種のディスパッチプロセスは、動的ディスパッチと呼ばれます。

2.2.3単一ディスパッチと複数ディスパッチ

受信者のメソッドとメソッドパラメータは、まとめてメソッド変数と呼ばれます分布の基になる数量の種類に応じて、分布は単一分布と複数分布の2つのタイプに分けることができます。2.2.1の静的ディスパッチの例に戻ります。この例では、メインオブジェクトが一意に決定されます。次に、コードが調整されます。

public class Main {

    static class A {
    }

    static class B extends A {
    }

    static class C extends B {
    }

    public void say(A a) {
        System.out.println("A");
    }

    public void say(B b) {
        System.out.println("B");
    }

    public void say(C c) {
        System.out.println("C");
    }

    public static void main(String[] args) throws Exception {
        Main main = new Main();
        Main superMain = new Super();
        B os = new C();
        main.say(os);
        superMain.say((A) os);
        //输出 B S-A
    }
}

class Super extends Main {
    public void say(A a) {
        System.out.println("S-A");
    }

    public void say(B b) {
        System.out.println("S-B");
    }

    public void say(C c) {
        System.out.println("S-C");
    }
}

main.say(os)およびsuperMain.sauy(os)の場合。

  • まず、コンパイル段階でのコンパイラの選択、つまり静的ディスパッチのプロセスを見てください。

現時点では、ターゲットメソッドの選択は2つのポイントに基づいています。1つはメソッドレシーバーの静的タイプがMainかSuperか、もう1つはメソッドパラメーターの静的タイプがBかCかです。mainとsuperMain(メソッドレシーバー)の静的タイプは両方ともMainであり、メソッドパラメーターの静的タイプは一方がBでもう一方がAであるためです。したがって、今回生成された2つのinvokevitrual命令のパラメーターは、それぞれ定数プール内のMain.say(B)およびMain.say(A)のメソッドへのシンボリック参照です。この選択は2つの引数に基づいているため、Java言語の静的ディスパッチは多重ディスパッチと呼ばれます。

  • 動的割り当てのプロセスであるランタイムフェーズでの仮想マシンの選択を見てみましょう

セクション2.2.2での動的ディスパッチの導入と上記の静的ディスパッチの結果から、main.say(os)およびsuperMain.say((A)os)のinvokevirtual命令が実行されると、メソッドシグネチャが実行されることがわかります。すでに静的にディスパッチされていますプロセスが確認されました。それぞれsay(B)とsay(A)である必要があります。現時点では、仮想マシンはパラメータの静的タイプと実際のタイプを気にしません。メソッドレシーバーの実際のタイプのみがメソッドバージョンの選択に影響します。つまり、選択の基準として引数が1つだけです。したがって、Java言語の動的ディスパッチは、単一のディスパッチタイプに属します。

したがって、現在のJava言語は、静的なマルチディスパッチ言語と動的なシングルディスパッチ言語です。

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

動的ディスパッチは非常に頻繁なアクションであり、動的ディスパッチのメソッドバージョン選択プロセスでは、実行時にクラスのメソッドメタデータで適切なターゲットメソッドを検索する必要があります。したがって、パフォーマンスを考慮して、仮想マシンが最適化されます。クラスメソッド領域にあります仮想メソッドテーブルの作成これに対応する仮想メソッドテーブルインターフェイスメソッドテーブル---インターフェイスメソッドテーブルは、invokeinterfaceの実行時にも使用されます。仮想メソッドテーブルには、各メソッドの実際のエントリアドレスが格納されます。 。

サブクラスでメソッドがオーバーライドされていない場合、サブクラスの仮想メソッドテーブルのアドレスエントリは、親クラスの同じメソッドのアドレスエントリと同じであり、それらはすべて親クラスの実装エントリを指します。 ;サブクラスがオーバーライドされた場合このメソッドの場合、サブクラスメソッドテーブルの仮想メソッドテーブルのアドレスは、サブクラスの実装バージョンを指すエントリアドレスに置き換えられます。このように、冗長ストレージを介して、実行時にターゲットメソッドを検索するときに、オブジェクトの各親クラスを順番に検索する必要はありません。

同時に、同じシグニチャを持つメソッドは、親クラスとサブクラスの仮想メソッドテーブルで同じインデックス番号を持つ必要があります。このように、型が変換されるとき、検索されるメソッドテーブルのみが必要です。変更され、別の仮想メソッドテーブルからのものである可能性があります。インデックスに従って目的のエントリアドレスを変換します。メソッドテーブルは通常、クラスロードの接続フェーズ(準備フェーズ)で初期化されます。クラス変数の初期値を準備した後、仮想マシンはクラスのメソッドテーブルも初期化します。

条件が許せば、仮想マシンはメソッドテーブルの使用に加えて、「クラス階層分析(CHA)」テクノロジに基づくインラインキャッシュと保護されたインライン化も使用します。より高いパフォーマンスを得る方法。

おすすめ

転載: blog.csdn.net/huangzhilin2015/article/details/114437682