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 方法的實體有四個構成:
- 方法名
- 簽名--引數列表和返回值
- 定義方法的類
- 方法體(程式碼)
同一個類中,方法名相同,簽名不同,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複製程式碼
前四個引數是固定的,被依次壓入操作棧裡
- MethodHandle,實際上這個方法控制程式碼就是指向 BSM
- Lookup, 也就是呼叫者,是 Indy 指令所在的類的上下文,可以通過
Lookup#lookupClass()
獲取這個類 - name ,lambda 所實現的方法名,也就是
"run"
- invokedType,呼叫點的方法簽名,這裡是
methodType(Runnable.class,String[].class)
接下來就是附加引數,這些引數是靈活的,由Bootstrap methods 表提供,這裡分別是:
- samMethodType,其實就是 Runnable.run 的描述符:
methodType(void.class)
。sam 就 single public abstract method 的縮寫 - implMethod: 編譯器給生成的 desugar 方法,是一個 MethodHandle:
caller.findStatic(LambdaTest.class,"lambda$main$0",methodType(void.class))
- 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
指令,這是運算元棧有兩個分別是:
- args[],lambda 裡面呼叫了 main 方法的引數
- 呼叫點物件(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 方法其實就是 selectMethod
。insertArguments
在這裡主要做了一個柯里化的操作,因為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 的應用才有一點大致的認識,文中如果有什麼錯誤,還請幫忙指出。