美文网首页java基础
invokedynamic指令和Lamada实现原理详解

invokedynamic指令和Lamada实现原理详解

作者: 程序员札记 | 来源:发表于2022-07-10 15:29 被阅读0次

    这个是invokedynamic指令实现的基础,本篇以Lamada的实现原理为例说明invokedynamic的具体用法以及C++字节码解释器中invokedynamic指令的源码实现。

    一、Lamada的实现原理
    1、字节码分析
    想看一个典型的Lamada表达式,具体用法可以参考《java8 Lambda表达式》,代码如下:

    package jni;
     
    import java.util.Arrays;
    import java.util.List;
    import java.util.function.Consumer;
     
    public class LamadaTest {
     
        public static void main(String[] args) {
            List<String> test= Arrays.asList("a","b","c");
            //完整的写法
    //        test.stream().forEach((String s)->{
    //            System.out.println("LamadaTest s->"+s);
    //        });
            //简写版,lamada自己做类型推断
            Consumer<String> consumer=s->System.out.println("LamadaTest s->"+s);
            test.stream().forEach(consumer);
        }
    }
    

    上述代码编译完成后执行javap -v查看具体的字节码指令,去除代码行号表等非关键部分后如下:

    Constant pool:
        #1 = Methodref          #18.#43       // java/lang/Object."<init>":()V
        #2 = Class              #44           // java/lang/String
        #3 = String             #45           // a
        #4 = String             #46           // b
        #5 = String             #47           // c
        #6 = Methodref          #48.#49       // java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;
        #7 = InvokeDynamic      #0:#55        // #0:accept:()Ljava/util/function/Consumer;
        #8 = InterfaceMethodref #56.#57       // java/util/List.stream:()Ljava/util/stream/Stream;
        #9 = InterfaceMethodref #58.#59       // java/util/stream/Stream.forEach:(Ljava/util/function/Consumer;)V
       #10 = Fieldref           #60.#61       // java/lang/System.out:Ljava/io/PrintStream;
       #11 = Class              #62           // java/lang/StringBuilder
       #12 = Methodref          #11.#43       // java/lang/StringBuilder."<init>":()V
       #13 = String             #63           // LamadaTest s->
       #14 = Methodref          #11.#64       // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
       #15 = Methodref          #11.#65       // java/lang/StringBuilder.toString:()Ljava/lang/String;
       #16 = Methodref          #66.#67       // java/io/PrintStream.println:(Ljava/lang/String;)V
       #17 = Class              #68           // jni/LamadaTest
       #18 = Class              #69           // java/lang/Object
       #19 = Utf8               <init>
       #20 = Utf8               ()V
       #21 = Utf8               Code
       #22 = Utf8               LineNumberTable
       #23 = Utf8               LocalVariableTable
       #24 = Utf8               this
       #25 = Utf8               Ljni/LamadaTest;
       #26 = Utf8               main
       #27 = Utf8               ([Ljava/lang/String;)V
       #28 = Utf8               args
       #29 = Utf8               [Ljava/lang/String;
       #30 = Utf8               test
       #31 = Utf8               Ljava/util/List;
       #32 = Utf8               consumer
       #33 = Utf8               Ljava/util/function/Consumer;
       #34 = Utf8               LocalVariableTypeTable
       #35 = Utf8               Ljava/util/List<Ljava/lang/String;>;
       #36 = Utf8               Ljava/util/function/Consumer<Ljava/lang/String;>;
       #37 = Utf8               lambda$main$0
       #38 = Utf8               (Ljava/lang/String;)V
       #39 = Utf8               s
       #40 = Utf8               Ljava/lang/String;
       #41 = Utf8               SourceFile
       #42 = Utf8               LamadaTest.java
       #43 = NameAndType        #19:#20       // "<init>":()V
       #44 = Utf8               java/lang/String
       #45 = Utf8               a
       #46 = Utf8               b
       #47 = Utf8               c
       #48 = Class              #70           // java/util/Arrays
       #49 = NameAndType        #71:#72       // asList:([Ljava/lang/Object;)Ljava/util/List;
       #50 = Utf8               BootstrapMethods
       #51 = MethodHandle       #6:#73        // invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
       #52 = MethodType         #74           //  (Ljava/lang/Object;)V
       #53 = MethodHandle       #6:#75        // invokestatic jni/LamadaTest.lambda$main$0:(Ljava/lang/String;)V
       #54 = MethodType         #38           //  (Ljava/lang/String;)V
       #55 = NameAndType        #76:#77       // accept:()Ljava/util/function/Consumer;
       #56 = Class              #78           // java/util/List
       #57 = NameAndType        #79:#80       // stream:()Ljava/util/stream/Stream;
       #58 = Class              #81           // java/util/stream/Stream
       #59 = NameAndType        #82:#83       // forEach:(Ljava/util/function/Consumer;)V
       #60 = Class              #84           // java/lang/System
       #61 = NameAndType        #85:#86       // out:Ljava/io/PrintStream;
       #62 = Utf8               java/lang/StringBuilder
       #63 = Utf8               LamadaTest s->
       #64 = NameAndType        #87:#88       // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
       #65 = NameAndType        #89:#90       // toString:()Ljava/lang/String;
       #66 = Class              #91           // java/io/PrintStream
       #67 = NameAndType        #92:#38       // println:(Ljava/lang/String;)V
       #68 = Utf8               jni/LamadaTest
       #69 = Utf8               java/lang/Object
       #70 = Utf8               java/util/Arrays
       #71 = Utf8               asList
       #72 = Utf8               ([Ljava/lang/Object;)Ljava/util/List;
       #73 = Methodref          #93.#94       // java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
       #74 = Utf8               (Ljava/lang/Object;)V
       #75 = Methodref          #17.#95       // jni/LamadaTest.lambda$main$0:(Ljava/lang/String;)V
       #76 = Utf8               accept
       #77 = Utf8               ()Ljava/util/function/Consumer;
       #78 = Utf8               java/util/List
       #79 = Utf8               stream
       #80 = Utf8               ()Ljava/util/stream/Stream;
       #81 = Utf8               java/util/stream/Stream
       #82 = Utf8               forEach
       #83 = Utf8               (Ljava/util/function/Consumer;)V
       #84 = Utf8               java/lang/System
       #85 = Utf8               out
       #86 = Utf8               Ljava/io/PrintStream;
       #87 = Utf8               append
       #88 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
       #89 = Utf8               toString
       #90 = Utf8               ()Ljava/lang/String;
       #91 = Utf8               java/io/PrintStream
       #92 = Utf8               println
       #93 = Class              #96           // java/lang/invoke/LambdaMetafactory
       #94 = NameAndType        #97:#101      // metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
       #95 = NameAndType        #37:#38       // lambda$main$0:(Ljava/lang/String;)V
       #96 = Utf8               java/lang/invoke/LambdaMetafactory
       #97 = Utf8               metafactory
       #98 = Class              #103          // java/lang/invoke/MethodHandles$Lookup
       #99 = Utf8               Lookup
      #100 = Utf8               InnerClasses
      #101 = Utf8               (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
      #102 = Class              #104          // java/lang/invoke/MethodHandles
      #103 = Utf8               java/lang/invoke/MethodHandles$Lookup
      #104 = Utf8               java/lang/invoke/MethodHandles
    {
      //构造方法
      public jni.LamadaTest();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
     
      //main方法
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=4, locals=3, args_size=1
             0: iconst_3   //将int常量3放到操作数栈的顶部
             1: anewarray     #2                  // class java/lang/String,使用操作数栈顶的数组长度和常年池中的数组元素类型java/lang/String构建一个新的数组,创建完成,栈顶是新的数组的引用
             4: dup        //将栈顶的值复制插入到栈顶,即此时操作数栈有两个新数组的引用,指向同一个数组                          
             5: iconst_0   //将int常量0放到操作数栈的顶部
             6: ldc           #3                  // String a  将运行时常年池的数据a放到操作数栈的顶部
             8: aastore  //从操作数栈顶部往下依次读取待插入的引用,插入的位置和插入的数组,将目标索引插入的指定数组的指定位置
             9: dup
            10: iconst_1
            11: ldc           #4                  // String b
            13: aastore  //同上,数组中插入b
            14: dup
            15: iconst_2
            16: ldc           #5                  // String c
            18: aastore   //同上数组中插入c,此时操作数栈顶只有一个数组的引用
            19: invokestatic  #6                  // Method java/util/Arrays.asList:([Ljava/lang/Object;)Ljava/util/List;调用静态方法,方法执行的时候会自动从操作数栈获取方法参数,方法执行完成结果保存在栈顶。#6表示invokestatic要执行的方法Methodref在常量池的索引
            22: astore_1    //将栈顶的引用类型的变量放入本地变量表中索引为1的位置
            23: invokedynamic #7,  0              // InvokeDynamic #0:accept:()Ljava/util/function/Consumer; 执行InvokeDynamic指令,#7表示常年池中的调用点限定符InvokeDynamic,执行完成栈顶放的就是Consumer的实例
            28: astore_2  //将栈顶的引用类型的变量放入本地变量表中索引为2的位置
            29: aload_1   //将本地变量表中索引为1的变量放到栈顶,即List对象
            30: invokeinterface #8,  1            // InterfaceMethod java/util/List.stream:()Ljava/util/stream/Stream; 调用stream方法,执行完成结果放到栈顶
            35: aload_2    //将本地变量表中索引为2的变量放到栈顶,即Consumer对象
            36: invokeinterface #9,  2            // InterfaceMethod java/util/stream/Stream.forEach:(Ljava/util/function/Consumer;)V  调用forEach方法
            41: return
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                0      42     0  args   [Ljava/lang/String;
               23      19     1  test   Ljava/util/List;
               29      13     2 consumer   Ljava/util/function/Consumer;
          LocalVariableTypeTable:
            Start  Length  Slot  Name   Signature
               23      19     1  test   Ljava/util/List<Ljava/lang/String;>;
               29      13     2 consumer   Ljava/util/function/Consumer<Ljava/lang/String;>;
    }
    SourceFile: "LamadaTest.java"
    //引用的内部类
    InnerClasses:
         public static final #99= #98 of #102; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
    //InvokeDynamic使用的
    BootstrapMethods:
      0: #51 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
        Method arguments:
          #52 (Ljava/lang/Object;)V
          #53 invokestatic jni/LamadaTest.lambda$main$0:(Ljava/lang/String;)V
          #54 (Ljava/lang/String;)V
    

    从上诉字节码的分析可知lamada实现的关键就在于invokedynamic指令和后面的调用点限定符了,下面逐一说明。

    2、调用点限定符
    调用点限定符在运行时常年池中的具体类型是CONSTANT_InvokeDynamic_info,专门为invokedynamic指令定制的,该指令的结构如下:


    image.png

    各项的具体含义如下:

    • tag: 标识这个常年池项的类型,一个常量值,CONSTANT_InvokeDynamic (18)
    • bootstrap_method_attr_index:对当前class文件中引导方法表的bootstrap_methods数组的有效索引
    • name_and_type_index:常量池的有效索引,对应常量池项必须是CONSTANT_NameAndType_info类型的,表示方法名和方法描述符。
      上述测试用例中对应的CONSTANT_InvokeDynamic_info常量如下:
    #7 = InvokeDynamic      #0:#55        // #0:accept:()Ljava/util/function/Consumer; 其中0就是引导方法表的索引
    #55 = NameAndType        #76:#77       // accept:()Ljava/util/function/Consumer;
    #76 = Utf8               accept
    #77 = Utf8               ()Ljava/util/function/Consumer;
    

    3、BootstrapMethods
    BootstrapMethods是一种变长属性,位于ClassFile结构的属性表中,用于保存invokedynamic指令使用的引导方法限定符。如果常量池中包含一个或者多个CONSTANT_InvokeDynamic_info类型的常量,则ClassFile结构的属性表必须包含且只能包含一个BootstrapMethods属性。该属性的结构如下:


    image.png

    各项的说明如下:

    • attribute_name_index:常量池的索引,该项必须是CONSTANT_Utf8_info类型,表示属性名,固定为BootstrapMethods
    • attribute_length:该属性的长度,即总的字节数,不包含初始的6个字节
    • num_bootstrap_methods:bootstrap_methods数组中包含的引导方法限定符的个数
    • bootstrap_methods:保存引导方法限定符的数组

    bootstrap_methods数组成员的结构说明如下:

    • bootstrap_method_ref:常量池的索引,该项必须是CONSTANT_MethodHandle_info类型,表示方法调用句柄
    • num_bootstrap_arguments:给出了下面的bootstrap_arguments数组的个数
    • bootstrap_arguments:常量池的索引数组,每个索引对应的常量池项都是bootstrap_method_ref对应的启动方法的入参,可以是CONSTANT_String_info, CONSTANT_Class_info, CONSTANT_Integer_info, CONSTANT_Long_info, CONSTANT_Float_info, CONSTANT_Double_info, CONSTANT_MethodHandle_info, or CONSTANT_MethodType_info类型。
      上述测试用例对应的引导方法表如下:
    引导方发表如下:
    BootstrapMethods:
      0: #51 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
        Method arguments:
          #52 (Ljava/lang/Object;)V
          #53 invokestatic jni/LamadaTest.lambda$main$0:(Ljava/lang/String;)V
          #54 (Ljava/lang/String;)V 
    常量池对应的项如下:
    #51 = MethodHandle       #6:#73        // invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    #52 = MethodType         #74           //  (Ljava/lang/Object;)V
    #53 = MethodHandle       #6:#75        // invokestatic jni/LamadaTest.lambda$main$0:(Ljava/lang/String;)V
    #54 = MethodType         #38           //  (Ljava/lang/String;)V
    

    4、CONSTANT_MethodType_info
    CONSTANT_MethodType_info是跟java.lang.invoke.MethodType类相对应的,其结构如下:


    image.png

    各项说明如下:

    • tag:该项的类型值,固定为CONSTANT_MethodType (16)
    • descriptor_index:常量池的索引,对应的项必须是CONSTANT_Utf8_info类型,表示MethodType对应的方法描述符
      上述测试用例中 CONSTANT_MethodType_info类型的项如下:
    #52 = MethodType         #74           //  (Ljava/lang/Object;)V
    #74 = Utf8               (Ljava/lang/Object;)V
     
    #54 = MethodType         #38           //  (Ljava/lang/String;)V
    #38 = Utf8               (Ljava/lang/String;)V
    

    5、CONSTANT_MethodHandle_info
    CONSTANT_MethodHandle_info跟java.lang.invoke.MethodHandle类相对应的,其结构如下:


    image.png

    各项说明如下:

    • tag:表示此常量池项的类型,固定值 CONSTANT_MethodHandle (15).

    • reference_kind:取值1-9之间,表示方法句柄的类型,该类型决定了执行方法调用的字节码

    • reference_index :对常量池的有效索引,跟reference_kind配合使用。
      reference_kind和reference_index 的对应关系如下:

    • 如果reference_kind的值为1 (REF_getField), 2 (REF_getStatic), 3 (REF_putField), or 4 (REF_putStatic),则对应的常量池索引项必须是CONSTANT_Fieldref_info,表示这是一个字段操作的句柄

    • 如果reference_kind的值是5 (REF_invokeVirtual) or 8 (REF_newInvokeSpecial)则对应的常量池项必须是CONSTANT_Methodref_info,表示这是一个执行普通方法或者构造方法的句柄,如果是8 (REF_newInvokeSpecial),则CONSTANT_Methodref_info的方法名称必须是<init>。

    • 如果reference_kind的值是 6 (REF_invokeStatic) or 7 (REF_invokeSpecial),且Class文件版本号大于52.0,即基于JDK8编译的,对应的常量池项必须是CONSTANT_Methodref_info structure or CONSTANT_InterfaceMethodref_info,表示这是一个执行类或者接口的某个方法的句柄

    • 如果reference_kind的值是 9 (REF_invokeInterface),则对应的常量池项必须是CONSTANT_InterfaceMethodref_info ,表示这是一个执行接口方法调用的句柄

    • 如果reference_kind的值是 5 (REF_invokeVirtual), 6 (REF_invokeStatic), 7 (REF_invokeSpecial), or 9 (REF_invokeInterface),则对应的CONSTANT_Methodref_info项中方法名不能是<init> or <clinit>.
      上述测试用例常量池中对应的CONSTANT_MethodHandle_info项如下:

    #这里第一个6就是 6 (REF_invokeStatic)
    #51 = MethodHandle       #6:#73        // invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
     
    #73 = Methodref          #93.#94       // java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
     
    #93 = Class              #96           // java/lang/invoke/LambdaMetafactory
    #94 = NameAndType        #97:#101      // metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
     
     #97 = Utf8               metafactory
     #101 = Utf8               (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    由上述分析可知,lamada  invokedynamic的启动方法就是java/lang/invoke/LambdaMetafactory.metafactory方法。
    

    6、LambdaMetafactory.metafactory
    metafactory的定义如下:

    image.png

    对应到上述测试用例,metafactory方法的传参如下:

    • caller:由JVM在执行启动方法前以当前类作为构造参数构造一个Lookup实例
    • invokedName:返回的CallSite关联的MethodHandle对应的方法名,invokedynamic指令下传入CONSTANT_InvokeDynamic_info类型中name_and_type_index对应的常量池项中的方法名,这里是accept:()Ljava/util/function/Consumer; 其中accept是方法名,()Ljava/util/function/Consumer是方法描述符。
    • invokedType:返回的CallSite关联的MethodHandle的方法描述符,同上传入()Ljava/util/function/Consumer,即没有入参,返回Consumer实例。

    上述三个参数及启动方法本身的MethodHandle实例的引用都会由JVM自动放入栈中,所以没有在启动方法的参数列表中显示,剩下的三个参数与启动方法的参数一一对应。

    • samMethodTyp:需要实现的lamada方法的MethodType,由JVM根据CONSTANT_MethodType_info常量池项自动构建一个对应的实例,这里是(Ljava/lang/Object;)V,即Consumer的accept的方法描述符。
    • implMethod:调用lamada方法具体实现方法的MethodHandle,这里是invokestatic jni/LamadaTest.lambdamain0:(Ljava/lang/String;)V,其中lambdamain0是方法名,(Ljava/lang/String;)V是方法描述符,即javac将lamada方法具体实现的这部分代码单独定义成了一个静态方- 法,方法名就是lambdamain0。
    • instantiatedMethodType:调用lamada方法的MethodType,这里是(Ljava/lang/String;)V
      7、CallSite
      CallSite称为调用点,实际是一个MethodHandle实例的Holder,该实例通过CallSite的target属性保存。invokedynamic指令会将所有的方法调用分发到该指令链接的CallSite对应的MethodHandle实例上,即执行MethodHandle实例关联的方法。一个CallSite可能与多个invokedynamic指令关联,也可能不跟任何invokedynamic指令关联,在任何情况下它都可能被一个通过它的dynamicInvoker方法返回的与之关联的MethodHandle调用,这个关联的MethodHandle相当于与当前CallSite关联的invokedynamic指令。

    CallSite是一个抽象类,不允许开发者继承CallSite实现自定义的子类,它有三个具体的可以实例化的子类,开发者可以继承这三个子类,分别是:

    • ConstantCallSite:如果CallSite对应的MethodHandle实例是不可变的,则使用ConstantCallSite,表示一个常量式的CallSite
    • VolatileCallSite:如果CallSite对应的MethodHandle实例是可变的,且要求具有volatile变量的效果,即一个线程改变了MethodHandle实例其他线程都能感知到,则使用VolatileCallSite。
    • MutableCallSite: 如果CallSite对应的MethodHandle实例是可变的,且不要求具有volatile变量的效果,则使用MutableCallSite,即当前线程改变了对应的MethodHandle实例,其他线程不可见。如果希望对其他线程可见,可手动调用syncAll方法。

    CallSite对应的MethodHandle实例发生改变时要求新的MethodHandle实例与原来的实例的MethodType保持一致,即无论MethodHandle实例怎么改变,CallSite关联的MethodType都保持不变。

    CallSite没有定义多余的方法和属性,比较简单,具体如下:

    image.png

    子类在CallSite基础上主要增加了公开的构造方法,如下:

    image.png

    8、InnerClassLambdaMetafactory
    InnerClassLambdaMetafactory就是实现Lamada的核心,从命名上可知还是使用内部类的形式实现的。构造方法会执行参数的初始化,validateMetafactoryArgs方法完成参数的校验,创建内部类和内部类实例的逻辑都在buildCallSite()方法中,具体如下:

    CallSite buildCallSite() throws LambdaConversionException {
            //生成lamada内部类
            final Class<?> innerClass = spinInnerClass();
            //返回的CallSite对应的方法的参数个数为0
            if (invokedType.parameterCount() == 0) {
                final Constructor<?>[] ctrs = AccessController.doPrivileged(
                        new PrivilegedAction<Constructor<?>[]>() {
                    @Override
                    public Constructor<?>[] run() {
                        //获取构造方法
                        Constructor<?>[] ctrs = innerClass.getDeclaredConstructors();
                        if (ctrs.length == 1) {
                            // The lambda implementing inner class constructor is private, set
                            // it accessible (by us) before creating the constant sole instance
                            ctrs[0].setAccessible(true);
                        }
                        return ctrs;
                    }
                        });
                //构造函数有多个抛出异常
                if (ctrs.length != 1) {
                    throw new LambdaConversionException("Expected one lambda constructor for "
                            + innerClass.getCanonicalName() + ", got " + ctrs.length);
                }
     
                try {
                    //创建一个新的lamada内部类实例
                    Object inst = ctrs[0].newInstance();
                    //constant方法返回一个特殊的MethodHandle,对其调用invoke方法,永远返回inst实例
                    //ConstantCallSite本身是对MethodHandle的包装而已
                    return new ConstantCallSite(MethodHandles.constant(samBase, inst));
                }
                catch (ReflectiveOperationException e) {
                    throw new LambdaConversionException("Exception instantiating lambda object", e);
                }
            } else {
                try {
                    //确保这个lamada内部类已经加载并初始化
                    UNSAFE.ensureClassInitialized(innerClass);
                    //NAME_FACTORY是lamada内部类返回实例的工厂方法的方法名,具体是get$Lambda
                    //invokedType是该方法的方法描述符,具体是()Ljava/util/function/Consumer
                    return new ConstantCallSite(
                            MethodHandles.Lookup.IMPL_LOOKUP
                                 .findStatic(innerClass, NAME_FACTORY, invokedType));
                }
                catch (ReflectiveOperationException e) {
                    throw new LambdaConversionException("Exception finding constructor", e);
                }
            }
        }
    

    spinInnerClass方法就是通过ASM字节码工具生成lamada内部类,具体如下:

    private Class<?> spinInnerClass() throws LambdaConversionException {
            String[] interfaces;
            //samBase是想实现的lamada接口类,samIntf表示接口名
            String samIntf = samBase.getName().replace('.', '/');
            boolean accidentallySerializable = !isSerializable && Serializable.class.isAssignableFrom(samBase);
            //markerInterfaces表示需要额外实现的用于标记的接口,比如java.io.Serializable接口,lamada下markerInterfaces为空数组
            if (markerInterfaces.length == 0) {
                //interfaces表示新生成的class需要实现的接口
                interfaces = new String[]{samIntf};
            } else {
                //确保没有重复的接口,所以使用HashSet
                Set<String> itfs = new LinkedHashSet<>(markerInterfaces.length + 1);
                itfs.add(samIntf);
                //将markerInterfaces中的接口加入到 interfaces中
                for (Class<?> markerInterface : markerInterfaces) {
                    itfs.add(markerInterface.getName().replace('.', '/'));
                    accidentallySerializable |= !isSerializable && Serializable.class.isAssignableFrom(markerInterface);
                }
                interfaces = itfs.toArray(new String[itfs.size()]);
            }
     
            //cw在构造函数中初始化的,用于构造Class的入口
            //添加接口类
            cw.visit(CLASSFILE_VERSION, ACC_SUPER + ACC_FINAL + ACC_SYNTHETIC,
                     lambdaClassName, null,
                     JAVA_LANG_OBJECT, interfaces);
     
            //添加字段
            for (int i = 0; i < argDescs.length; i++) {
                FieldVisitor fv = cw.visitField(ACC_PRIVATE + ACC_FINAL,
                                                argNames[i],
                                                argDescs[i],
                                                null, null);
                fv.visitEnd();
            }
            //生成构造函数
            generateConstructor();
            //如果invokedType的参数不止一个,则生成返回lamada实例的工厂方法
            if (invokedType.parameterCount() != 0) {
                generateFactory();
            }
     
            //生成目标方法
            MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, samMethodName,
                                              samMethodType.toMethodDescriptorString(), null, null);
            //添加方法注解                                  
            mv.visitAnnotation("Ljava/lang/invoke/LambdaForm$Hidden;", true);
            new ForwardingMethodGenerator(mv).generate(samMethodType);
     
            //生成用于bridge的方法,lamada下additionalBridges为空数组
            if (additionalBridges != null) {
                for (MethodType mt : additionalBridges) {
                    mv = cw.visitMethod(ACC_PUBLIC|ACC_BRIDGE, samMethodName,
                                        mt.toMethodDescriptorString(), null, null);
                    mv.visitAnnotation("Ljava/lang/invoke/LambdaForm$Hidden;", true);
                    new ForwardingMethodGenerator(mv).generate(mt);
                }
            }
     
            //生成序列化方法
            if (isSerializable)
                generateSerializationFriendlyMethods();
            else if (accidentallySerializable)
                generateSerializationHostileMethods();
     
            cw.visitEnd();
     
            // 转换成二进制数组
            final byte[] classBytes = cw.toByteArray();
     
            //如果要求的话将生成的内部类dump出来
            if (dumper != null) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    @Override
                    public Void run() {
                        dumper.dumpClass(lambdaClassName, classBytes);
                        return null;
                    }
                }, null,
                new FilePermission("<<ALL FILES>>", "read, write"),
                // createDirectories may need it
                new PropertyPermission("user.dir", "read"));
            }
            //加载该内部类
            return UNSAFE.defineAnonymousClass(targetClass, classBytes, null);
        }
    

    上述生成内部类的代码留下了一个问题,lamada方法的具体实现的字节码是怎么跟内部类的目标方法绑定的?lamada方法的具体实现就是一开始调用metafactory方法的入参implMethod。通过搜索implMethod在代码中出现的地方,最终在一个意想不到的地方找到答案,ForwardingMethodGenerator类,该类用于生成具体的lamada方法,原以为该类是ASM定义的标准类,其实是InnerClassLambdaMetafactory的内部类,其关键实现如下:

    //TypeConvertingMethodAdapter是ASM定义的类
    private class ForwardingMethodGenerator extends TypeConvertingMethodAdapter {
     
            ForwardingMethodGenerator(MethodVisitor mv) {
                super(mv);
            }
     
            void generate(MethodType methodType) {
                visitCode();
     
                //implKind取自implMethod的调用类型,上述测试用例中是invokestatic
                if (implKind == MethodHandleInfo.REF_newInvokeSpecial) {
                    visitTypeInsn(NEW, implMethodClassName);
                    visitInsn(DUP);
                }
                //处理方法参数
                for (int i = 0; i < argNames.length; i++) {
                    visitVarInsn(ALOAD, 0);
                    visitFieldInsn(GETFIELD, lambdaClassName, argNames[i], argDescs[i]);
                }
     
                convertArgumentTypes(methodType);
     
                // Invoke the method we want to forward to
                //这部分就是关键了,调用我们期望调用方法,implMethodClassName,implMethodName和implMethodDesc都是从implMethod中取出的属性
                //即目标调用类,方法名,方法描述符,即ASM生成的方法实现实际只是对代码中定义的具体实现的一个调用而已,并未将实现代码对应的字节码
                //转移到生成的类中
                visitMethodInsn(invocationOpcode(), implMethodClassName,
                                implMethodName, implMethodDesc,
                                implDefiningClass.isInterface());
     
                // 对方法调用结果做转换
                Class<?> samReturnClass = methodType.returnType();
                convertType(implMethodReturnClass, samReturnClass, samReturnClass);
                visitInsn(getReturnOpcode(samReturnClass));
                // Maxs computed by ClassWriter.COMPUTE_MAXS,these arguments ignored
                visitMaxs(-1, -1);
                visitEnd();
            }
        }
    

    9、总结
    通过执行BootstrapMethods属性中的启动方法,可以获取一个CallSite实例,该实例的target是一个获取一个已实现目标lamada接口(上述测试用例中是java/util/function/Consumer接口)的实例的MethodHandle,invokedynamic调用与之关联的CallSite的MethodHandle实例得到一个Consumer实例,将其放到操作数栈的栈顶,至此invokedynamic指令调用完成。

    实现目标lamada接口的类是通过ASM字节码工具在运行时生成的,并非内部类。源代码中实现目标lamada接口的代码会被编译成一个新的隐藏的方法,上述测试用例中因为方法实现中未使用实例变量所以被编译成了一个静态方法。新生成的类实现目标lamada接口方法时实际就只是调用了这个隐藏的方法而已,然后做必要参数和返回值的类型转换。

    二、invokedynamic指令
    1、使用总结
    结合上面lamada实现原理的分析,再来回顾《Java虚拟机规范》中invokedynamic指令的描述,就很好懂了,这里总结如下:

    每个invokedynamic指令出现的地方称之为一个动态调用点,每个动态调用点都有对应的调用点限定符,即CONSTANT_InvokeDynamic_info。解析调用点限定符中的bootstrap_method_attr_index属性可以获取调用对应启动方法的MethodHandle实例,解析调用点限定符中的name_and_type_index属性获取与目标CallSite关联的MethodType实例。调用点限定符解析完成,会将下列实例按照顺序依次方法操作数栈中:

    • 代表调用引导方法的MethodHandle实例的引用
    • lookup class是当前动态调用点所在的类的Lookup实例的引用
    • 指向调用点限定符的name_and_type_index属性包含的方法名的String实例的引用
    • 指向调用点限定符的name_and_type_index属性包含的方法描述符的MethodType实例的引用
    • 启动方法中其他参数的引用,按照bootstrap_arguments项中定义的顺序依次入栈

    入栈完成,JVM会执行启动方法,假如启动方法的对应的符号引用是R,JVM对启动方法有如下要求:

    • R是类中方法的符号引用
    • R对应的类必须是java.lang.invoke.MethodHandle
    • R对应的调用方法必须是invoke
    • R对应的方法返回值类型必须是java.lang.invoke.CallSite
    • 前3个参数类型必须是java.lang.invoke.MethodHandles.Lookup,String和java.lang.MethodType,其余静态参数按照入栈的顺序追加到方法描述符的参数类型的后面。

    当第一次解析此动态调用点对应的调用限定符时会触发引导方法的调用,如果同时有多个线程尝试解析,则引导方法会被并发调用,因此如果引导方法中需要访问全局数据,则应该做好对全局数据方法的并发访问控制。引导方法执行完成会返回一个java.lang.CallSite或者其子类的实例,这个对象称为调用点对象,此对象的引用将会从操作数栈中出栈,然后发布到所有线程中。如果多个线程同时执行同一个动态调用点的引导方法则JVM必须选择其中的一个返回结果发布到所有线程中,其他线程返回的调用点对象会被直接忽略。调用点对象的MethodType实例必须与从调用限定符中解析出来的MethodType实例一样,调用点对象发布到所有线程后,会与这个动态调用点永久绑定。当执行到此invokedynamic指令时,JVM会实际调用对应CallSite的MethodHandle实例的invokeExact方法,此时操作数栈的内容会被解释成一个指向调用点目标对象的引用,即MethodHandle实例和方法调用的N个参数,这些参数的类型,数量和顺序都与调用限定符中包含的方法描述符保持一致。

    2、源码解析:
    参考OpenJDK8 hotspot/src/share/vm/interpreter/bytecodeInterpreter.cpp 2444行的实现,源码说明如下:

    CASE(_invokedynamic): {
            //如果禁用了InvokeDynamic抛出异常
            if (!EnableInvokeDynamic) {
              // We should not encounter this bytecode if !EnableInvokeDynamic.
              // The verifier will stop it.  However, if we get past the verifier,
              // this will stop the thread in a reasonable way, without crashing the JVM.
              CALL_VM(InterpreterRuntime::throw_IncompatibleClassChangeError(THREAD),
                      handle_exception);
              ShouldNotReachHere();
            }
            
            //往后读取4字节的字节码数据
            u4 index = Bytes::get_native_u4(pc+1);
            //读取常量池中的调用限定符的解析结果
            ConstantPoolCacheEntry* cache = cp->constant_pool()->invokedynamic_cp_cache_entry_at(index);
     
            // We are resolved if the resolved_references field contains a non-null object (CallSite, etc.)
            // This kind of CP cache entry does not need to match the flags byte, because
            // there is a 1-1 relation between bytecode type and CP entry type.
            //如果还未解析,则调用InterpreterRuntime::resolve_invokedynamic方法完成解析
            if (! cache->is_resolved((Bytecodes::Code) opcode)) {
              CALL_VM(InterpreterRuntime::resolve_invokedynamic(THREAD),
                      handle_exception);
              cache = cp->constant_pool()->invokedynamic_cp_cache_entry_at(index);
            }
            //解析完成f1包含一个CallSite对象,作为一个appendix,f1_as_method()方法返回的Method就是对应的MethodHandle绑定的方法
            Method* method = cache->f1_as_method();
            if (VerifyOops) method->verify();
     
            if (cache->has_appendix()) {
              ConstantPool* constants = METHOD->constants();
              //将CallSite对象的引用放入栈顶
              SET_STACK_OBJECT(cache->appendix_if_resolved(constants), 0);
              MORE_STACK(1);
            }
     
            istate->set_msg(call_method);
            //执行方法调用
            istate->set_callee(method);
            istate->set_callee_entry_point(method->from_interpreted_entry());
            //将字节码指针往后移动5个字节
            istate->set_bcp_advance(5);
     
            //profile统计
            BI_PROFILE_UPDATE_CALL();
            //更新PC计数器并返回
            UPDATE_PC_AND_RETURN(0); // I'll be back...
          }
    InterpreterRuntime::resolve_invokedynamic方法的源码说明如下:
    
    //第一次调用时会解析调用限定符,创建一个永久绑定的CallSite
    IRT_ENTRY(void, InterpreterRuntime::resolve_invokedynamic(JavaThread* thread)) {
      assert(EnableInvokeDynamic, "");
      const Bytecodes::Code bytecode = Bytecodes::_invokedynamic;
     
     
      // resolve method
      CallInfo info;
      //从当前线程正在执行的方法获取该方法所属的类的常量池
      constantPoolHandle pool(thread, method(thread)->constants());
      //从字节码中读取调用限定符在常量池中的索引
      int index = get_index_u4(thread, bytecode);
      {
        JvmtiHideSingleStepping jhss(thread);
        //解析调用限定符,解析的结果保存在CallInfo中,其中_resolved_method属性就是CallSite最终关联的方法
        LinkResolver::resolve_invoke(info, Handle(), pool,
                                     index, bytecode, CHECK);
      } // end JvmtiHideSingleStepping
     
      //将解析结果保存到常量池中
      ConstantPoolCacheEntry* cp_cache_entry = pool->invokedynamic_cp_cache_entry_at(index);
      cp_cache_entry->set_dynamic_call(pool, info);
    }
    IRT_END
    

    相关文章

      网友评论

        本文标题:invokedynamic指令和Lamada实现原理详解

        本文链接:https://www.haomeiwen.com/subject/bqjwbrtx.html