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

目次

1.Java動的型言語のサポート

1.1 MethodHandle

1.2MethodHandleとReflectionの違い

1.3invokedynamic命令

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

2.1スタックベースおよびレジスタベース

2.2スタックベースのインタプリタ実行プロセス

3、まとめ


1.Java動的型言語のサポート

動的に型付けされた言語の主な機能は、型チェックの主なプロセスがコンパイル時ではなく実行時に行われることです。これとは対照的に、JavaScript、Pythonなど、この機能を満たす多くの言語があります。コンパイル時に型チェックを実行するもの(C ++ / Javaなど)は、最も一般的に使用される静的型付け言語です。

たとえば、次のコード:

obj.println("hello world");

このコード行がJava言語であり、変数objの静的型がjava.io.PrintStreamであるとすると、変数objの実際の型はPrintStreamのサブクラス(PrintStreamインターフェイスの実装)である必要があります。それ以外の場合は、objが実行する場合でも有効なprintln(String)メソッドがありますが、PrintStreamインターフェイスとの継承関係はなく、型チェックが不正であるため、コードを実行できません。

JavaScriptの同じコードは異なります。objのタイプに関係なく、このタイプの定義にprintln(String)メソッドが含まれている限り、メソッド呼び出しは成功します。

この違いの理由は、Java言語がコンパイル時にprintln(String)メソッドの完全なシンボリック参照を生成し、次のコードのように、メソッド呼び出し命令のパラメーターとしてクラスファイルに格納するためです。

invokevirtual #4//Method java/io/printStream.println:(Ljava/lang/String;)V

このシンボリック参照には、メソッドが定義されている特定のタイプ、メソッドの名前、パラメーターの順序、パラメーターのタイプ、メソッドの戻り値などの情報が含まれています。このシンボリック参照を通じて、仮想マシンはこのメソッドの直接参照を変換できます。 。JavaScriptなどの動的に型付けされた言語では、変数obj自体には型がなく、変数objの値には型があります。コンパイル時には、メソッド名、パラメーター、および戻り値のみを決定できます。特定の方法を決定する代わりに。

静的に型付けされた言語はコンパイル時に型を決定するため、コンパイラーは厳密な型チェックを提供できますこれは安定性に役立ちます。動的言語は実行時に型を決定します。これはより柔軟であり、関数を実装するときに、より明確で静的に型付けされた言語よりも明確です。簡潔に言えば、コードはそれほど「肥大化」して表示されません。

これらのメソッドのinvokevirtual、invokespecial、invokestatic、およびinvokeinterface命令の最初のパラメーターは、呼び出されたメソッドのシンボリック参照であり、シンボリック参照はコンパイル時に生成されるため、動的型言語はでメソッドレシーバー型のみを判別できます。そのため、サポートを提供するために、新しいinvokedynamicコマンドがJDK1.7に追加されました。

1.1 MethodHandle

JDK1.7は、呼び出されるターゲットメソッドを決定するためにシンボリック参照に単純に依存していた以前のメソッドに加えて、MethodHandleと呼ばれるターゲットメソッドを動的に決定するための新しいメカニズムを提供しますその機能は基本的にクラス内のメソッドシグネチャに一致するメソッドを見つけることであり、結果はMethodHandleで表され、メソッドはMethodHandleを介して呼び出すことができます。このように、コードは仮想マシンのディスパッチとメソッドの検索のプロセスをシミュレートするために使用され、プログラマーに高い自由度を与えます。

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

public class Main {
    static class ClassA {
        public void println(String arg) {
            System.out.println(arg);
        }
    }

    public static void main(String[] args) throws Throwable {
        Object obj = new ClassA();
        getMethodHandle(obj).invoke("hello MethodHandle");
    }

    private static MethodHandle getMethodHandle(Object receiver) throws Exception {
        //定义一个MethodType
        //第一个参数(void.class)是方法的返回类型,后面的参数(String.class)是方法的参数
        MethodType methodType = MethodType.methodType(void.class, String.class);
        //在receiver.class中寻找方法
        //并且通过bindTo将该方法的接收者(也就是this)传递给它
        return MethodHandles.lookup()
                .findVirtual(receiver.getClass(), "println", methodType)
                .bindTo(receiver);
    }
}

上記のサンプルコードは、invokeVirtual命令の実行プロセスをシミュレートするinvokeVirtualを使用しています。その他には、findStatic、findSpecialなどがあります。

1.2MethodHandleとReflectionの違い

  • 本質的に、ReflectionメカニズムとMethodメカニズムはどちらもメソッド呼び出しをシミュレートしていますが、ReflectionはJavaコードレベルでメソッド呼び出しをシミュレートしています。MethodHandles.lookupの3つのメソッド:findStatic()、findVirtual()、およびfindSpecial()は、 invokestatic、invokevirtual&invokeinterface、invokespecialのいくつかのバイトコード命令の実行権限検証動作。ReflectionAPIを使用する場合、これらの低レベルの詳細を考慮する必要はありません。

  • 反射があるヘビー。Mehodオブジェクトは、メソッドのシグネチャ、記述子、およびメソッド属性テーブルにおける各種特性のJavaの側の表現を含んでいる。また、このような実行権限としてランタイム情報を含みます。MethodHandleには、メソッド名やパラメーターなど、メソッドの実行に関連する情報のみが含まれ、比較的軽量です。

  • MethodHandleはバイトコードメソッド実行命令のシミュレーションであるため、理論的には、この領域の仮想マシンによって行われるさまざまな最適化(メソッドのインライン化など)も、MethodHandleの同様のアイデアによってサポートされる必要があり、Reflectionを介してメソッドを呼び出すことはできません。

  • Reflectionの設計目標はJava言語のみを提供することですが、MethodHandleはJava仮想マシン上のすべての言語提供するように設計されています

1.3invokedynamic命令

invokedynamic命令とMethodHandleメカニズムの機能は同じです。どちらも、仮想マシンで固定されている元の4つのメソッド呼び出し命令ディスパッチングルールの問題を解決し、ターゲットメソッドを見つける方法に関する決定を仮想マシンから転送します。特定のユーザーコードへの仮想マシン。その中で、ユーザーがより高い自由度を持つことができます。2つの考え方も似ていますが、1つは上位レベルのJavaコードとAPIを使用して実現し、もう1つはバイトコードとクラス内の他の属性と定数を使用して完了します。

Invokedynamicのすべての位置に命令が含まれている場合、「動的呼び出しサイト」(動的呼び出しサイトと呼ばれます。この命令は、CONSTANT_Methodref_info定数シンボリック参照メソッドを表す最初の命令ではありませんが、JDK1.7への新規参入者はCONSTANT_InvokeDynamic_info定数です。この新しい情報は、3:ガイダンス方法タイプ名前の方法で取得できますbootメソッドは固定パラメーターであり、戻り値はjava.lang.invoke.CallSiteオブジェクトであり、実行されるターゲットメソッド呼び出しを表します。CONSTANT_InvokeDynamic_infoを介して、ブートメソッドを見つけて実行し、それによってCallSiteオブジェクトを取得し、最後に実行するターゲットメソッドを呼び出すことができます。

invokedynamicと前の4つのinvoke *命令の最大の違いは、そのディスパッチロジックが仮想マシンではなく、プログラマーによって決定されることです。この命令のユーザーはJava言語ではなく、他のJava仮想マシン上の動的言語であるため、Java言語ベースのコンパイラjavacは、invokedynamic命令でバイトコードを生成できません。

このセクションの最後に、非常に興味深い質問が本に記載されています。サブクラスの祖父母クラスのオーバーライドメソッドを呼び出す方法は?

import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

public class Main {
    static class GrandFather {
        void thinking() {
            System.out.println("i am grandfather");
        }
    }

    static class Father extends GrandFather {
        void thinking() {
            System.out.println("i am father");
        }
    }

    static class Son extends Father {
        void thinking() {
           //只完善这个方法的代码,实现调用祖父类的thinking()方法,打印"i am grandfather"
        }
    }

    public static void main(String[] args) throws Throwable {
        Son son = new Son();
        son.thinking();
    }

}

注:もちろん、これを埋めることはできません。newGrandFather()。thinking();

JDK1.7以前は、invokevirtual命令の動的ディスパッチがレシーバーの実際のタイプを使用するため、方法がありませんでした。このロジックは仮想マシンで固められますが、Sonクラスのthinkingメソッドでは実際のタイプを取得できません。参照GrandFatherのオブジェクトに(インスタンス化しない限り)。ただし、JDK1.7以降では、MethodHandleを使用できます。

static class Son extends Father {
        void thinking() {
            try {
                MethodType methodType = MethodType.methodType(void.class);
                Field IMPL_LOOKUP = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
                IMPL_LOOKUP.setAccessible(true);
                ((MethodHandles.Lookup) IMPL_LOOKUP.get(null)).findSpecial(GrandFather.class, "thinking", methodType, Father.class)
                        .invoke(this);
            } catch (Throwable e) {
                e.printStackTrace();
            }
        }
    }

MethodHandleを使用して、invokespecial命令をシミュレートし、GrandFaher.classから自分の希望に応じて思考メソッドを見つけ、独自のディスパッチロジックを完成させます。

注:本に記載されている解決策は次のとおりです。

MethodType methodType = MethodType.methodType(void.class);
MethodHandles.lookup().findSpecial(GrandFather.class, "thinking", methodType, this.getClass())
    .bindTo(this)
    .invoke();

ただし、コードがJDK1.7および1.8で期待される効果を達成できないことが確認されています。ブロガーはLookupのソースコードを見てIMPL_LOOKUPの使い方を見つけました。詳しくはコメント欄をご覧ください〜

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

Java言語では、javacコンパイラは字句解析->文法解析->抽象構文木->構文木をトラバースして線形バイトコード命令ストリームを生成するプロセスを完了し、インタプリタは仮想マシン内にあります。

2.1スタックベースおよびレジスタベース

Javaコンパイラによって出力される命令ストリームは、基本的にスタックベースの命令セットアーキテクチャです。命令ストリーム内のほとんどの命令はゼロアドレス命令であり、動作するためにオペランドスタックに依存しています。対照的に、命令セットアーキテクチャの別のセットはレジスタベースの命令セットであり、これらの命令はレジスタに依存して機能します。

「1+ 1」の結果を計算する場合、スタックベースの命令フローは次のようになります。

iconst_1 //int类型的1入栈
iconst_1 //int类型的1入栈
iadd //栈顶两个int类型出栈,相加,把结果入栈
istore_0 //将栈顶的值出栈放到局部变量表的第0位置的slot中

レジスタに基づいている場合は、次のようになります。

mov eax,1 //把eax寄存器的值设为1
add eax,1 //把eax寄存器的值加1,结果保存在eax寄存器

スタックベースの命令セットは移植可能であり、レジスタはハードウェアによって直接的または間接的に提供されます。これらのハードウェアレジスタに依存するプログラムはハードウェアの制約を受けますが、スタックベースの命令セットは同じ機能を完了するために、より多くの命令を必要とします。レジスタアーキテクチャ。多くの場合、スタックはメモリに実装されます。頻繁なスタックアクセスは頻繁なメモリアクセスを意味します。プロセッサと比較して、メモリは常に実行速度のボトルネックです。よる指示メモリアクセスは、スタックアーキテクチャの命令セットの実行速度は比較的遅いです。すべての主流の物理マシンの命令セットは、レジスタに基づいています。

2.2スタックベースのインタプリタ実行プロセス

ここでは本の同じサンプルコードを使用していますが、便宜上、図は描かず、代わりに、各命令の後にオペランドスタックとローカル変数テーブルの状態をテキストとともに記録します。Javaコードは次のとおりです。

public int calc() {
        int a = 100;
        int b = 200;
        int c = 300;
        return (a + b) * c;
    }

コンパイル後、javapを介してバイトコード命令を表示します(オペランドスタックとローカル変数テーブルの変更は、スタックが説明されている注釈にあり、右側はスタックの最上位の方向です):

public int calc();
    descriptor: ()I
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        100  //将100入栈。栈:100;变量表:0=this
         2: istore_1           //将100出栈,存放到局部变量表第1个slot。栈:空;变量表:0=this,1=100
         3: sipush        200  //将200入栈。栈:200;变量表:0=this,1=100
         6: istore_2           //将200出栈,存放到局部变量表第2个slot。栈:空;变量表:0=this,1=100,2=200
         7: sipush        300  //将300入栈。栈:300;变量表:0=this,1=100,2=200
        10: istore_3           //将300出栈,存放到局部变量表第3个slot。栈:空;变量表:0=this,1=100,2=200,3=300
        11: iload_1            //将局部变量表中第1个slot整型值入栈。栈:100;变量表:0=this,1=100,2=200,3=300
        12: iload_2            //将局部变量表中第2个slot整型值入栈。栈:100,200;变量表:0=this,1=100,2=200,3=300
        13: iadd               //将栈顶两个元素出栈做整型加法,然后把结果入栈。栈:300;变量表:0=this,1=100,2=200,3=300
        14: iload_3            //将局部变量表中第3个slot整型值入栈。栈:300,300;变量表:0=this,1=100,2=200,3=300
        15: imul               //将栈顶两个元素出栈做整型乘法,然后把结果入栈。栈:90000;变量表:0=this,1=100,2=200,3=300
        16: ireturn            //结束方法执行,将栈顶整型值返回给方法调用者
      LineNumberTable:
        line 8: 0
        line 9: 3
        line 10: 7
        line 11: 11
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      17     0  this   Lcom/demo/Main;
            3      14     1     a   I
            7      10     2     b   I
           11       6     3     c   I

上記から、このコードには、深さ2のオペランドスタック(ポップ/プッシュプロセス中のスタックの最大深さを参照)と、ローカル変数スペースの4つのスロット(this、a、b、 c)

各命令の意味については、公式ドキュメントを参照できます:https//docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html

上記の実行プロセスは単なる概念モデルであり、仮想マシンは最終的に実行プロセスを最適化してパフォーマンスを向上させます。実際の操作プロセスは、概念モデルとは大きく異なる場合があります。仮想マシンのパーサーとジャストインタイムコンパイラの両方が入力バイトコードを最適化するため、たとえばHotSpot仮想マシンでは、入力バイトをマージして置換するために「fast_」で始まる多くの非標準バイトコード命令があります。解釈と実行のパフォーマンスを向上させるためのコード、およびジャストインタイムコンパイラの最適化方法はより多様です(次の章で紹介します)。

3、まとめ

上記の内容の多くはJava仮想マシンの概念モデルに基づいていますが、実際の状況とはある程度のギャップがありますが、これは仮想マシンの原理の理解を妨げるものではありません。

おすすめ

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