方法引用(Method reference)和invokedynamic指令詳細分析

racaljk發表於2019-01-04

方法引用(Method reference)和invokedynamic指令詳細分析

invokedynamic是jvm指令集裡面最複雜的一條。本文將詳細分析invokedynamic指令是如何實現方法引用(Method reference)的。

具體言之,有這樣一個方法引用:

interface Encode {
    void encode(Derive person);
}
class Base {
    public void encrypt() {
        System.out.println("Base::speak");
    }
}
class Derive extends Base {
    @Override
    public void encrypt() {
        System.out.println("Derive::speak");
    }
}
public class MethodReference {
    public static void main(String[] args) {
        Encode encode = Base::encrypt;
        System.out.println(encode);
    }
}

使用javap -verbose MethodReference.class檢視對應位元組碼:

// 常量池
Constant pool:
   #1 = Methodref          #6.#22         // java/lang/Object."<init>":()V
   #2 = InvokeDynamic      #0:#27         // #0:encode:()LEncode;
   #3 = Fieldref           #28.#29        // java/lang/System.out:Ljava/io/PrintStream;
   #4 = Methodref          #30.#31        // java/io/PrintStream.println:(Ljava/lang/Object;)V
   #5 = Class              #32            // MethodReference
   #6 = Class              #33            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               LMethodReference;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               encode
  #19 = Utf8               LEncode;
  #20 = Utf8               SourceFile
  #21 = Utf8               MethodReference.java
  #22 = NameAndType        #7:#8          // "<init>":()V
  #23 = Utf8               BootstrapMethods
  #24 = MethodHandle       #6:#34         // invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;L
java/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang
/invoke/CallSite;
  #25 = MethodType         #35            //  (LDerive;)V
  #26 = MethodHandle       #5:#36         // invokevirtual Base.encrypt:()V
  #27 = NameAndType        #18:#37        // encode:()LEncode;
  #28 = Class              #38            // java/lang/System
  #29 = NameAndType        #39:#40        // out:Ljava/io/PrintStream;
  #30 = Class              #41            // java/io/PrintStream
  #31 = NameAndType        #42:#43        // println:(Ljava/lang/Object;)V
  #32 = Utf8               MethodReference
  #33 = Utf8               java/lang/Object
  #34 = Methodref          #44.#45        // java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/Str
ing;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallS
ite;
  #35 = Utf8               (LDerive;)V
  #36 = Methodref          #46.#47        // Base.encrypt:()V
  #37 = Utf8               ()LEncode;
  #38 = Utf8               java/lang/System
  #39 = Utf8               out
  #40 = Utf8               Ljava/io/PrintStream;
  #41 = Utf8               java/io/PrintStream
  #42 = Utf8               println
  #43 = Utf8               (Ljava/lang/Object;)V
  #44 = Class              #48            // java/lang/invoke/LambdaMetafactory
  #45 = NameAndType        #49:#53        // metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Lj
ava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
  #46 = Class              #54            // Base
  #47 = NameAndType        #55:#8         // encrypt:()V
  #48 = Utf8               java/lang/invoke/LambdaMetafactory
  #49 = Utf8               metafactory

// 位元組碼指令
 public static void main(java.lang.String[]);
     0: invokedynamic #2,  0              // InvokeDynamic #0:encode:()LEncode;
     5: astore_1
     6: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
     9: aload_1
    10: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
    13: return

// 屬性
SourceFile: "MethodReference.java"
InnerClasses:
     public static final #51= #50 of #56; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
  0: #24 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/Method
Type;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #25 (LDerive;)V
      #26 invokevirtual Base.encrypt:()V
      #25 (LDerive;)V

使用invokedynamic指令生成encode物件,然後存入區域性變數槽#1。接著獲取getstatic獲取java/lang/System類的out欄位,最後區域性變數槽#1作為引數壓棧,invokevirtual虛擬函式呼叫System.outprintln方法。

那麼invokedynamic到底是怎麼生成encode物件的呢?

1.虛擬機器解析

hotspotinvokedynamic指令的解釋如下:

      CASE(_invokedynamic): {

        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.
        if (! cache->is_resolved((Bytecodes::Code) opcode)) {
          CALL_VM(InterpreterRuntime::resolve_from_cache(THREAD, (Bytecodes::Code)opcode),
                  handle_exception);
          cache = cp->constant_pool()->invokedynamic_cp_cache_entry_at(index);
        }

        Method* method = cache->f1_as_method();
        if (VerifyOops) method->verify();

        if (cache->has_appendix()) {
          ConstantPool* constants = METHOD->constants();
          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());
        istate->set_bcp_advance(5);

        // Invokedynamic has got a call counter, just like an invokestatic -> increment!
        BI_PROFILE_UPDATE_CALL();

        UPDATE_PC_AND_RETURN(0); // I`ll be back...
      }

使用invokedynamic_cp_cache_entry_at獲取常量池物件,然後檢查是否已經解析過,如果沒有就解析反之複用,然後設定方法位元組碼,留待後面解釋執行。那麼,重點是這個解析。我們對照著jvm spec來看。

根據jvm文件的描述,invokedynamic的運算元(operand)指向常量池一個動態呼叫點描述符(dynamic call site specifier)。
動態呼叫點描述符是一個CONSTANT_InvokeDynamic_info結構體:

CONSTANT_InvokeDynamic_info {
 u1 tag;
 u2 bootstrap_method_attr_index;
 u2 name_and_type_index;
}
  • tag 表示這個結構體的常量,不用管
  • bootstrap_method_attr_index 啟動方法陣列
  • name_and_type_index 一個名字+型別的描述欄位,就像這樣Object p放到虛擬機器裡面表示是Ljava/lang/Object; p

然後啟動方法陣列結構是這樣:

BootstrapMethods_attribute {
 ...
 u2 num_bootstrap_methods;
 { 
    u2 bootstrap_method_ref;
    u2 num_bootstrap_arguments;
    u2 bootstrap_arguments[num_boot]
    } bootstrap_methods[num_bootstrap_methods];
}

就是一個陣列,每個元素是{指向MethodHandle的索引,啟動方法引數個數,啟動方法引數}

MethodlHandle是個非常重要的結構,指導了虛擬機器對於這個啟動方法的解析,先關注一下這個結構:

CONSTANT_MethodHandle_info {
 u1 tag;//表示該結構體的常量tag,可以忽略
 u1 reference_kind;
 u2 reference_index;
}
  • reference_kind是[1,9]的數,它表示這個method handle的型別,這個欄位和位元組碼的行為有關。
  • reference_index 根據reference_kind會指向常量池的不同型別,具體來說
    • reference_kind==1,3,4 指向CONSTANT_Fieldref_info結構,表示一個類的欄位
    • reference_kind==5,8,指向CONSTANT_Methodref_info,表示一個類的方法
    • reference_kind==6,7, 同上,只是兼具介面的方法或者類的方法的可能。
    • reference_kind==9,指向CONSTATN_InterfaceMethodref_info,表示一個介面方法

通過invokedynamic,我們可以得

  1. 名字+描述符的表示(由name_and_type_index給出)
  2. 一個啟動方法陣列(由bootstrap_method_attr_index給出)

2.手動解析

可以手動模擬一下解析,看看最後得到的資料是什麼樣的。在這個例子中:

  0: invokedynamic #2,  0   //第二個operand總是0

檢視常量池#2項:

#2 = InvokeDynamic      #0:#27         // #0:encode:()LEncode;
#27 = NameAndType        #18:#37        // encode:()LEncode;

BootstrapMethods:
  0: #24 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/Method
Type;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #25 (LDerive;)V
      #26 invokevirtual Base.encrypt:()V
      #25 (LDerive;)V

得到的名字+描述符是:Encode.encode(),啟動方法陣列有一個元素,回憶下之前說的,這個元素構成如下:

{指向MethodHandle的索引,啟動方法引數個數,啟動方法引數}

這裡得到的MethodHandle表示的是LambdaMetafactory.metafactory:

#24 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/Method
Type;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;`

啟動方法引數有:

  • #25 (LDerive;)V
  • #26 invokevirtual Base.encrypt:()V
  • #25 (LDerive;)V

3. java.lang.invoke.LambdaMetafactory

先說說LambdaMetafactory有什麼用。javadoc給出的解釋是:

Facilitates the creation of simple “function objects” that implement one or more interfaces by delegation to a provided MethodHandle, after appropriate type adaptation and partial evaluation of arguments. Typically used as a bootstrap method for invokedynamic call sites, to support the lambda expression and method reference expression features of the Java Programming Language.
When the target of the CallSite returned from this method is invoked, the resulting function objects are instances of a class which implements the interface named by the return type of invokedType, declares a method with the name given by invokedName and the signature given by samMethodType. It may also override additional methods from Object.

LambdaMetafactory方便我們建立簡單的”函式物件”,這些函式物件通過代理MethodHandle實現了一些介面。
當這個函式返回的CallSite被呼叫的時候,會產生一個類的例項,該類還實現了一些方法,具體由引數給出

將上面得到的MethodHandle寫得更可讀就是呼叫的這個方法:

   public static CallSite LambdaMetafactory.metafactory(MethodHandles.Lookup caller,
                                       String invokedName,
                                       MethodType invokedType,
                                       MethodType samMethodType,
                                       MethodHandle implMethod,
                                       MethodType instantiatedMethodType);

六個引數,慢慢來。

3.1 LambdaMetafactory.metafactory()呼叫前

要知道引數是什麼意思,可以從它的呼叫者來管中窺豹:

 static CallSite makeSite(MethodHandle bootstrapMethod,
                             // Callee information:
                             String name, MethodType type,
                             // Extra arguments for BSM, if any:
                             Object info,
                             // Caller information:
                             Class<?> callerClass) {
        MethodHandles.Lookup caller = IMPL_LOOKUP.in(callerClass);
        CallSite site;
        try {
            Object binding;
            info = maybeReBox(info);
            if (info == null) {
                binding = bootstrapMethod.invoke(caller, name, type);
            } else if (!info.getClass().isArray()) {
                binding = bootstrapMethod.invoke(caller, name, type, info);
            } else {
                Object[] argv = (Object[]) info;
                maybeReBoxElements(argv);
                switch (argv.length) {
                ...
                case 3:
                    binding = bootstrapMethod.invoke(caller, name, type,
                                                     argv[0], argv[1], argv[2]);
                    break;
                ...
                }
            }
            //System.out.println("BSM for "+name+type+" => "+binding);
            if (binding instanceof CallSite) {
                site = (CallSite) binding;
            }  else {
                throw new ClassCastException("bootstrap method failed to produce a CallSite");
            }
            ...
        } catch (Throwable ex) {
            ...
        }
        return site;
    }

java.lang.invoke.LambdaMetafactory的呼叫是通過MethodHandle引發的,所以可能還需要補一下MethodHandle的用法,百度一搜一大堆,javadoc也給出了使用示例:

String s;
MethodType mt; MethodHandle mh;
MethodHandles.Lookup lookup = MethodHandles.lookup();
// mt is (char,char)String
mt = MethodType.methodType(String.class, char.class, char.class);
mh = lookup.findVirtual(String.class, "replace", mt);
s = (String) mh.invoke("daddy",`d`,`n`);
// invokeExact(Ljava/lang/String;CC)Ljava/lang/String;
assertEquals(s, "nanny");

回到原始碼,關鍵是這句:

binding = bootstrapMethod.invoke(caller, name, type,
                               argv[0], argv[1], argv[2]);

argv[0],argv[1],argv[2]分別表示之前啟動方法的三個引數
caller即呼叫者,這裡是MethodReference這個類,然後name和type參見下面的詳細解釋:

  • MethodHandles.Lookup caller 表示哪個類引發了調動
  • String invokedName 表示生成的類的方法名,對應例子的encode
  • MethodType invokedType 表示CallSite的函式簽名,其中引數型別表示捕獲變數的型別,返回型別是類要實現的介面的名字,對應例子的()Encode,即要生成一個類,這個類沒有捕獲自由變數(所以引數類為空),然後這個類要實現Encode介面(返回型別為生成的類要實現的介面)
    接下來
  • MethodType samMethodType 表示要實現的方法的函式簽名和返回值,對於例子的#25 (LDerive;)V,即實現方法帶有一個形參,返回void
  • MethodHandle implMethod 表示實現的方法裡面應該呼叫的函式,對於例子的#26 invokevirtual Base.encrypt:()V,表示呼叫Base的虛擬函式encrypt,返回void
  • MethodType instantiatedMethodType 表示呼叫方法的執行時描述符,如果不是泛型就和samMethodType一樣

3.2 LambdaMetafactory.metafactory()呼叫

原始碼面前,不是了無祕密嗎hhh,點進原始碼看看這個LambdaMetafactory到底做了什麼:

     */
    public static CallSite metafactory(MethodHandles.Lookup caller,
                                       String invokedName,
                                       MethodType invokedType,
                                       MethodType samMethodType,
                                       MethodHandle implMethod,
                                       MethodType instantiatedMethodType)
            throws LambdaConversionException {
        AbstractValidatingLambdaMetafactory mf;
        mf = new InnerClassLambdaMetafactory(caller, invokedType,
                                             invokedName, samMethodType,
                                             implMethod, instantiatedMethodType,
                                             false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
        mf.validateMetafactoryArgs();
        return mf.buildCallSite();
    }

它什麼也沒做,做事的是InnerClassLambdaMetafactory.buildCallSite()建立的最後CallSite,那就進一步看看InnerClassLambdaMetafactory.buildCallSite()

    @Override
    CallSite buildCallSite() throws LambdaConversionException {
        // 1. 建立生成的類物件
        final Class<?> innerClass = spinInnerClass();
        if (invokedType.parameterCount() == 0) {
            // 2. 用反射獲取建構函式
            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 {
                // 3. 建立例項 
                Object inst = ctrs[0].newInstance();
                // 4. 根據例項和samBase(介面型別)生成MethodHandle
                // 5. 生成ConstantCallSite
                return new ConstantCallSite(MethodHandles.constant(samBase, inst));
            }
            catch (ReflectiveOperationException e) {
                throw new LambdaConversionException("Exception instantiating lambda object", e);
            }
        } else {
            try {
                UNSAFE.ensureClassInitialized(innerClass);
                return new ConstantCallSite(
                        MethodHandles.Lookup.IMPL_LOOKUP
                             .findStatic(innerClass, NAME_FACTORY, invokedType));
            }
            catch (ReflectiveOperationException e) {
                throw new LambdaConversionException("Exception finding constructor", e);
            }
        }
    }

首先它生成一個.class檔案,虛擬機器預設不會輸出,需要下面設定VM option-Djdk.internal.lambda.dumpProxyClasses=.,Dump出虛擬機器生成的類我得到的是:

import java.lang.invoke.LambdaForm.Hidden;

// $FF: synthetic class
final class MethodReference$$Lambda$1 implements Encode {
    private MethodReference$$Lambda$1() {
    }

    @Hidden
    public void encode(Derive var1) {
        ((Base)var1).encrypt();
    }
}

該類實現了傳來的介面函式(動態類生成,熟悉spring的朋友應該很熟悉)。

回到buildCallSite()原始碼,它使用MethodHandles.constant(samBase, inst)建立MethdHandle,放到CallSite裡面,完成整個LambdaMetafactory的工作。
MethodHandles.constant(samBase, inst)相當於一個總是返回inst的方法。

總結

到這裡就結束了整個流程,文章有點長,總結一下:

  1. 虛擬機器遇到invokedynamic,開始解析運算元
  2. 根據invokedynamic #0:#27獲取到啟動方法(#0)和一個名字+描述符(#27)
    其中啟動方法是
BootstrapMethods:
  0: #24 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/Method
Type;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #25 (LDerive;)V
      #26 invokevirtual Base.encrypt:()V
      #25 (LDerive;)V

名字+描述符

 #27 = NameAndType        #18:#37        // encode:()LEncode;
  1. 啟動方法指向LambdaMetafactory.metafactory,但是不會直接呼叫而是通過MethdHandle間接呼叫。呼叫位置位於CallSite.makeCallSite()
  2. LambdaMetafactory.metafactory()其實使用InnerClassLambdaMetafactory.buildCallSite()建立了最後的CallSite
  3. buildCallSite()會建立一個.class,
  4. buildCallSite()會向最後的CallSite裡面放入一個可呼叫的MethdHandle
  5. 這個MethodHandle指向的是一個總是返回剛剛建立的.class類的例項的方法,由MethodHandles.constant(samBase, inst)完成
  6. 最後,用invokevirtual呼叫CallSite裡面的MethdHandle,返回.class類的示例,即inst,即new MethodReference$$Lambda$1

相關文章