理解 invokedynamic

TiouLims發表於2017-10-12

inDy(invokedynamic)是 java 7 引入的一條新的虛擬機器指令,這是自 1.0 以來第一次引入新的虛擬機器指令。到了 java 8 這條指令才第一次在 java 應用,用在 lambda 表示式中。 indy 與其他 invoke 指令不同的是它允許由應用級的程式碼來決定方法解析。所謂應用級的程式碼其實是一個方法,在這裡這個方法被稱為引導方法(Bootstrap Method),簡稱 BSM。BSM 返回一個 CallSite(呼叫點) 物件,這個物件就和 inDy 連結在一起了。以後再執行這條 inDy 指令都不會建立新的 CallSite 物件。CallSite 就是一個 MethodHandle(方法控制程式碼)的 holder。方法控制程式碼指向一個呼叫點真正執行的方法。

理解 MethodHandle(方法控制程式碼)的一種方式就是將其視為以安全、現代的方式來實現反射的核心功能。

一個 java 方法的實體有四個構成:

  1. 方法名
  2. 簽名--引數列表和返回值
  3. 定義方法的類
  4. 方法體(程式碼)

同一個類中,方法名相同,簽名不同,JVM 會視為不同的方法,不過在 Java 中只支援簽名的引數列表部分,也就是過載多型。一次方法呼叫,除了要方法的實體外,還要呼叫者(caller)和接收者(receiver),呼叫者也就是方法呼叫語句所在的類。接收者是一個物件,每個方法呼叫都要一個接收者,它可以是隱藏的(this),也可以是類方法,比如: String.valueOf,類也是 Class 的一個例項。

MethodType 表示方法簽名。

用 MethodHandle 實現的方法呼叫的示例如下,可以看到方法的四個構成:

Object rcvr = "a";
try {
    MethodType mt = MethodType.methodType(int.class); // 方法簽名
    MethodHandles.Lookup l = MethodHandles.lookup(); // 呼叫者,也就是當前類。呼叫者決定有沒有許可權能訪問到方法
    MethodHandle mh = l.findVirtual(rcvr.getClass(), "hashCode", mt); //分別是定義方法的類,方法名,簽名

    int ret;
    try {
        ret = (int)mh.invoke(rcvr); // 程式碼,第一個引數就是接收者
        System.out.println(ret);
    } catch (Throwable t) {
        t.printStackTrace();
    }
} catch (IllegalArgumentException | NoSuchMethodException | SecurityException e) {
    e.printStackTrace();
} catch (IllegalAccessException x) {
    x.printStackTrace();
}複製程式碼

詳細可參考:

java8 lambda 表示式

lambda 表示式 是怎麼使用 inDy 呢?以一段簡單的程式碼為例

public class LambdaTest {
    public static void main(String[] args) {
        Runnable r = () -> System.out.println(Arrays.toString(args));
        r.run();
    }
}複製程式碼

javap -v -p LambdaTest 檢視位元組碼,可以發現寥寥幾行 java 程式碼生成的位元組碼卻不少,單單常量池常量就有 66 個之多。輸出見 LambdaTest.class

可以發現多出了一個新方法,方法體就是 lambda 體(lambda body),轉換為原始碼如下:

private static void lambda$main$0(java.lang.String[] args){
    System.out.println(Arrays.toString(args));
}複製程式碼

主要看一下 main 方法,並沒有直接呼叫上面的方法,而是出現一條 inDy 指令:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=2, args_size=1
         0: aload_0
         1: invokedynamic #2,  0              // InvokeDynamic #0:run:([Ljava/lang/String;)Ljava/lang/Runnable;
         6: astore_1
         7: aload_1
         8: invokeinterface #3,  1            // InterfaceMethod java/lang/Runnable.run:()V
        13: return複製程式碼

可以看到 inDy 指向一個型別為 CONSTANT_InvokeDynamic_info 的常量項 #2,另外 0 是預留引數,暫時沒有作用。

#2 = InvokeDynamic      #0:#30         // #0:run:([Ljava/lang/String;)Ljava/lang/Runnable;複製程式碼

#0 表示在 Bootstrap methods 表中的索引:

BootstrapMethods:
  0: #27 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:
      #28 ()V
      #29 invokestatic com/company/LambdaTest.lambda$main$0:([Ljava/lang/String;)V
      #28 ()V複製程式碼

#30 則是一個 CONSTANT_NameAndType_info,表示方法名和方法型別(返回值和引數列表),這個會作為引數傳遞給 BSM。

#30 = NameAndType        #43:#44        // run:([Ljava/lang/String;)Ljava/lang/Runnable;複製程式碼

再看回表中的第 0 項,#27 是一個 CONSTANT_MethodHandle_info,實際上是個 MethodHandle(方法控制程式碼)物件,這個控制程式碼指向的就是 BSM 方法。在這裡就是:

java.lang.invoke.LambdaMetafactory.metafactory(MethodHandles.Lookup,String,MethodType,MethodType,MethodHandle,MethodType)複製程式碼

BSM 前三個引數是固定的,後面還可以附加任意數量的引數,但是引數的型別是有限制的,引數型別只能是

  • String
  • Class
  • int
  • long
  • float
  • double
  • MethodHandle
  • MethodType

LambdaMetafactory.metafactory 帶多三個引數,這些的引數的值由 Bootstrap methods 表 提供:

Method arguments:
  #25 ()V
  #26 invokestatic com/company/LambdaTest.lambda$main$0:()V
  #25 ()V複製程式碼

inDy 所需要的資料大概就是這些,可參考 Java8學習筆記(2) -- InvokeDynamic指令 - CSDN部落格

inDy 執行時

每一個 inDy 指令都稱為 Dynamic Call Site(動態呼叫點),根據 jvm 規範所說的,inDy 可以分為兩步,這兩步部分程式碼程式碼是在 java 層的,給 metafactory 方法設斷點可以看到一些行為。

第一步 inDy 需要一個 CallSite(呼叫點物件),CallSite 是由 BSM 返回的,所以這一步就是呼叫 BSM 方法。程式碼可參考:java.lang.invoke.CallSite#makeSite

呼叫 BSM 方法可以看作 invokevirtual 指令執行一個 invoke 方法,方法簽名如下:

invoke:(MethodHandle,Lookup,String,MethodType,/*其他附加靜態引數*/)CallSite複製程式碼

前四個引數是固定的,被依次壓入操作棧裡

  1. MethodHandle,實際上這個方法控制程式碼就是指向 BSM
  2. Lookup, 也就是呼叫者,是 Indy 指令所在的類的上下文,可以通過 Lookup#lookupClass()獲取這個類
  3. name ,lambda 所實現的方法名,也就是"run"
  4. invokedType,呼叫點的方法簽名,這裡是 methodType(Runnable.class,String[].class)

接下來就是附加引數,這些引數是靈活的,由Bootstrap methods 表提供,這裡分別是:

  1. samMethodType,其實就是 Runnable.run 的描述符: methodType(void.class)。sam 就 single public abstract method 的縮寫
  2. implMethod: 編譯器給生成的 desugar 方法,是一個 MethodHandle:caller.findStatic(LambdaTest.class,"lambda$main$0",methodType(void.class))
  3. instantiatedMethodType: Runnable.run 執行時的描述符,如果方法泛型的,那這個型別可能不一樣。這裡是 methodType(void.class)

上面說的固定其實應該是指 inDy 傳遞的實參型別是固定的,BSM 形參宣告可以是隨意,保證 BSM 能被呼叫就行,比如說 Lookup 宣告為 Object 不影響呼叫。

接下來就是執行 LambdaMetafactory.metafactory 方法了,它會建立一個匿名類,這個類是通過 ASM 編織位元組碼在記憶體中生成的,然後直接通過 unsafe 直接載入而不會寫到檔案裡。不過可以通過下面的虛擬機器引數讓它執行的時候輸出到檔案

-Djdk.internal.lambda.dumpProxyClasses=<path>複製程式碼

這個類是根據 lambda 的特點生成的,輸出後可以看到,在這個例子中是這樣的:

import java.lang.invoke.LambdaForm.Hidden;

// $FF: synthetic class
final class LambdaTest$$Lambda$1 implements Runnable {
    private final String[] arg$1;

    private LambdaTest$$Lambda$1(String[] var1) {
        this.arg$1 = var1;
    }

    private static Runnable get$Lambda(String[] var0) {
        return new LambdaTest$$Lambda$1(var0);
    }

    @Hidden
    public void run() {
        LambdaTest.lambda$main$0(this.arg$1);
    }
}複製程式碼

然後就是建立一個 CallSite,繫結一個 MethodHandle,指向的方法其實就是生成的類中的靜態方法 LambdaTest$$Lambda$1.get$Lambda(String[])Runnable。然後把呼叫點物件返回,到這裡 BSM 方法執行完畢。

更詳細的可參考:

第二步,就是執行這個方法控制程式碼了,這個過程就像 invokevirtual 指令執行 MethodHandle#invokeExact 一樣,

加上 inDy 上面那一條 aload_0 指令,這是運算元棧有兩個分別是:

  1. args[],lambda 裡面呼叫了 main 方法的引數
  2. 呼叫點物件(CallSite),實際上是方法控制程式碼。如果是 CostantCallSite 的時候,inDy 會直接跟他的方法控制程式碼連結。見程式碼:MethodHandleNatives.java#L255

傳入 args,執行方法,返回一個 Runnable 物件,壓入棧頂。到這裡 inDy 就執行完畢。

接下來的指令就很好理解,astore_1 把棧頂的 Runnable 物件放到區域性變數表的槽位1,也是變數 r。剩下的就是再拿出來呼叫 run 方法。

Groovy

接下來看一下 groovy 是如何使用 inDy 指令的。先複習一遍 groovy 的方法派發。

每當 Groovy 呼叫一個方法時,它不會直接呼叫它,而是要求一箇中間層來代替它。 中間層通過鉤子方法允許我們更改方法呼叫的行為。這個中間層就是 MOP(meta object proctol),MOP 主要承載的類就是 MetaClass 。一個簡化版的 MOP 主要有這些方法:

  • invokeMethod(String methodName, Object args)
  • methodMissing(String name, Object arguments)
  • getProperty(String propertyName)
  • setProperty(String propertyName, Object newValue)
  • propertyMissing(String name)

可以大致認為在 Groovy 中的每個方法和屬性訪問呼叫都會轉化上面的方法呼叫。而這些方法可以在執行時通過重寫修改它的預設行為,MOP 作為方法派發的中心樞紐為 Groovy 提供了非常靈活的動態程式設計的能力。

現在來看一下一段簡短的 groovy 程式碼,

class Test{
  int a = 0;
  static void main(args){
      Test wtf = new Test()
      wtf.a
      wtf.doSomething()
  }
}複製程式碼

通過 groovyc -indy Test.groovy 把它編譯成位元組碼。 indy 選項的意思就是啟用 invokedynamic 支援。

看一下編譯後的 main 方法。

public static void main(java.lang.String...);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
    Code:
      stack=1, locals=2, args_size=1
         0: ldc           #2                  // class Test
         2: invokedynamic #44,  0             // InvokeDynamic #0:init:(Ljava/lang/Class;)Ljava/lang/Object;
         7: invokedynamic #50,  0             // InvokeDynamic #1:cast:(Ljava/lang/Object;)LTest;
        12: astore_1
        13: aload_1
        14: pop
        15: aload_1
        16: invokedynamic #56,  0             // InvokeDynamic #2:getProperty:(LTest;)Ljava/lang/Object;
        21: pop
        22: aload_1
        23: invokedynamic #61,  0             // InvokeDynamic #3:invoke:(LTest;)Ljava/lang/Object;
        28: pop
        29: return複製程式碼

可以看到一共有 4 條 inDy 指令,包括建構函式,訪問成員變數,和不存在的方法呼叫都是 通過 invokedynamic 實現的。

再看一下引導方法表

BootstrapMethods:
  0: #38 invokestatic org/codehaus/groovy/vmplugin/v7/IndyInterface.bootstrap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;I)Ljava/lang/invoke/CallSite;
    Method arguments:
      #39 <init>
      #40 0
  1: #38 invokestatic org/codehaus/groovy/vmplugin/v7/IndyInterface.bootstrap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;I)Ljava/lang/invoke/CallSite;
    Method arguments:
      #46 ()
      #40 0
  2: #38 invokestatic org/codehaus/groovy/vmplugin/v7/IndyInterface.bootstrap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;I)Ljava/lang/invoke/CallSite;
    Method arguments:
      #51 a
      #52 4
  3: #38 invokestatic org/codehaus/groovy/vmplugin/v7/IndyInterface.bootstrap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;I)Ljava/lang/invoke/CallSite;
    Method arguments:
      #58 doSomething
      #40 0複製程式碼

可以發現所有 inDy 指令的引導方法都是 IndyInterface.bootstrap

以方法呼叫的 inDy 指令為例,它的方法名稱是 "invoke",方法簽名是 methodType(Object.class,Test.class),BSM 方法還附帶兩個引數分別是實際的方法名:"doSomething" 和一個標誌:0

BSM 方法最終呼叫的是 realBootstrap 方法:

private static CallSite realBootstrap(Lookup caller, String name, int callID, MethodType type, boolean safe, boolean thisCall, boolean spreadCall) {
    MutableCallSite mc = new MutableCallSite(type); //這裡是 MutableCallSite,lambda 表示式用的是 ConstantCallSite
    MethodHandle mh = makeFallBack(mc,caller.lookupClass(),name,callID,type,safe,thisCall,spreadCall);
    mc.setTarget(mh);
    return mc;
}複製程式碼

主要的程式碼是呼叫 makeFallBack 來獲取一個臨時的 MethodHandle。因為在第一步 groovy 無法確定接收者(receiver),也是就是 invoke 方法的第一個實參(Test 例項),必須要在第二步確定 CallSite 後才會傳遞過來。所以方法解析要放在第二步。

protected static MethodHandle makeFallBack(MutableCallSite mc, Class<?> sender, String name, int callID, MethodType type, boolean safeNavigation, boolean thisCall, boolean spreadCall) {
    MethodHandle mh = MethodHandles.insertArguments(SELECT_METHOD, 0, mc, sender, name, callID, safeNavigation, thisCall, spreadCall, /*dummy receiver:*/ 1); //MethodHandle(Object.class,Object[].class)
    mh =    mh.asCollector(Object[].class, type.parameterCount()).
            asType(type);
    return mh;
}複製程式碼

這個 fallback 方法其實就是 selectMethodinsertArguments 在這裡主要做了一個柯里化的操作,因為selectMethod 的方法簽名是

methodType(Object.class, MutableCallSite.class, Class.class, String.class, int.class, Boolean.class, Boolean.class, Boolean.class, Object.class, Object[].class)複製程式碼

而 inDy 要求的方法簽名卻是

methodType(Object.class,Test.class)。複製程式碼

所以得經過 insertArguments 的變換,把確定的值填充進去,用最後的陣列引數來接收 inDy 傳遞的引數。這樣這個方法就能夠被 inDy 呼叫了。第一步建立 CallSite 到這裡就結束。

第二步,就是 selectMethod 方法的呼叫,這時候 groovy 已經知道方法的接收者 arguments[0]

public static Object selectMethod(MutableCallSite callSite, Class sender, String methodName, int callID, Boolean safeNavigation, Boolean thisCall, Boolean spreadCall, Object dummyReceiver, Object[] arguments) throws Throwable {
    Selector selector = Selector.getSelector(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, arguments); 
    selector.setCallSiteTarget();

    MethodHandle call = selector.handle.asSpreader(Object[].class, arguments.length);
    call = call.asType(MethodType.methodType(Object.class,Object[].class));
    return call.invokeExact(arguments);
}複製程式碼

首先建立一個方法解析器,在這裡是 MethodSelector。接著呼叫 setCallSiteTarget(),這個方法就是用來解析實際的方法。具體的過程還是很複雜的,所以也沒法說清楚,大體來說就是確定接收者的 MetaClass,決定這個方法是實際的方法,還是交給 MetaClass 的鉤子方法,然後就是建立這個方法的 MethodHandle,然後把這個 MethodHandle 的簽名轉化為要求的簽名。這時 selecor.handle 就是最終呼叫的方法控制程式碼了。接下來就是最終的方法呼叫了,到這裡 inDy 指令就執行完畢了。

還有一個方法值得留意:

public void doCallSiteTargetSet() {
    if (!cache) {
        if (LOG_ENABLED) LOG.info("call site stays uncached");
    } else {
        callSite.setTarget(handle);
        if (LOG_ENABLED) LOG.info("call site target set, preparing outside invocation");
    }
}複製程式碼

這也是為什麼用 MutableCallSite 的原因,如果編譯器認為這個方法是可以快取,那麼就會把這個 CallSite 繫結到實際的 MethodHandle,後續的呼叫就不用再重新解析了。

最後

沒有相關經驗,inDy 還是很不好理解的,學習了 java 8 和 groovy 對 inDy 的應用才有一點大致的認識,文中如果有什麼錯誤,還請幫忙指出。