inDy(invokedynamic)是 java 7 引入的一條新的虛擬機(jī)指令,這是自 1.0 以來(lái)第一次引入新的虛擬機(jī)指令。到了 java 8 這條指令才第一次在 java 應(yīng)用,用在 lambda 表達(dá)式中。 indy 與其他 invoke 指令不同的是它允許由應(yīng)用級(jí)的代碼來(lái)決定方法解析。所謂應(yīng)用級(jí)的代碼其實(shí)是一個(gè)方法,在這里這個(gè)方法被稱為引導(dǎo)方法(Bootstrap Method),簡(jiǎn)稱 BSM。BSM 返回一個(gè) CallSite(調(diào)用點(diǎn)) 對(duì)象,這個(gè)對(duì)象就和 inDy 鏈接在一起了。以后再執(zhí)行這條 inDy 指令都不會(huì)創(chuàng)建新的 CallSite 對(duì)象。CallSite 就是一個(gè) MethodHandle(方法句柄)的 holder。方法句柄指向一個(gè)調(diào)用點(diǎn)真正執(zhí)行的方法。
理解 MethodHandle(方法句柄)的一種方式就是將其視為以安全、現(xiàn)代的方式來(lái)實(shí)現(xiàn)反射的核心功能。
一個(gè) java 方法的實(shí)體有四個(gè)構(gòu)成:
- 方法名
- 簽名--參數(shù)列表和返回值
- 定義方法的類
- 方法體(代碼)
同一個(gè)類中,方法名相同,簽名不同,JVM 會(huì)視為不同的方法,不過(guò)在 Java 中只支持簽名的參數(shù)列表部分,也就是重載多態(tài)。一次方法調(diào)用,除了要方法的實(shí)體外,還要調(diào)用者(caller)和接收者(receiver),調(diào)用者也就是方法調(diào)用語(yǔ)句所在的類。接收者是一個(gè)對(duì)象,每個(gè)方法調(diào)用都要一個(gè)接收者,它可以是隱藏的(this),也可以是類方法,比如: String.valueOf,類也是 Class 的一個(gè)實(shí)例。
MethodType 表示方法簽名。
用 MethodHandle 實(shí)現(xiàn)的方法調(diào)用的示例如下,可以看到方法的四個(gè)構(gòu)成:
Object rcvr = "a";
try {
MethodType mt = MethodType.methodType(int.class); // 方法簽名
MethodHandles.Lookup l = MethodHandles.lookup(); // 調(diào)用者,也就是當(dāng)前類。調(diào)用者決定有沒(méi)有權(quán)限能訪問(wèn)到方法
MethodHandle mh = l.findVirtual(rcvr.getClass(), "hashCode", mt); //分別是定義方法的類,方法名,簽名
int ret;
try {
ret = (int)mh.invoke(rcvr); // 代碼,第一個(gè)參數(shù)就是接收者
System.out.println(ret);
} catch (Throwable t) {
t.printStackTrace();
}
} catch (IllegalArgumentException | NoSuchMethodException | SecurityException e) {
e.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
}
詳細(xì)可參考:
java8 lambda 表達(dá)式
lambda 表達(dá)式 是怎么使用 inDy 呢?以一段簡(jiǎn)單的代碼為例
public class LambdaTest {
public static void main(String[] args) {
Runnable r = () -> System.out.println(Arrays.toString(args));
r.run();
}
}
用 javap -v -p LambdaTest 查看字節(jié)碼,可以發(fā)現(xiàn)寥寥幾行 java 代碼生成的字節(jié)碼卻不少,單單常量池常量就有 66 個(gè)之多。輸出見(jiàn) LambdaTest.class。
可以發(fā)現(xiàn)多出了一個(gè)新方法,方法體就是 lambda 體(lambda body),轉(zhuǎn)換為源碼如下:
private static void lambda$main$0(java.lang.String[] args){
System.out.println(Arrays.toString(args));
}
主要看一下 main 方法,并沒(méi)有直接調(diào)用上面的方法,而是出現(xiàn)一條 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 指向一個(gè)類型為 CONSTANT_InvokeDynamic_info 的常量項(xiàng) #2,另外 0 是預(yù)留參數(shù),暫時(shí)沒(méi)有作用。
#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 則是一個(gè) CONSTANT_NameAndType_info,表示方法名和方法類型(返回值和參數(shù)列表),這個(gè)會(huì)作為參數(shù)傳遞給 BSM。
#30 = NameAndType #43:#44 // run:([Ljava/lang/String;)Ljava/lang/Runnable;
再看回表中的第 0 項(xiàng),#27 是一個(gè) CONSTANT_MethodHandle_info,實(shí)際上是個(gè) MethodHandle(方法句柄)對(duì)象,這個(gè)句柄指向的就是 BSM 方法。在這里就是:
java.lang.invoke.LambdaMetafactory.metafactory(MethodHandles.Lookup,String,MethodType,MethodType,MethodHandle,MethodType)
BSM 前三個(gè)參數(shù)是固定的,后面還可以附加任意數(shù)量的參數(shù),但是參數(shù)的類型是有限制的,參數(shù)類型只能是
- String
- Class
- int
- long
- float
- double
- MethodHandle
- MethodType
LambdaMetafactory.metafactory 帶多三個(gè)參數(shù),這些的參數(shù)的值由 Bootstrap methods 表 提供:
Method arguments:
#25 ()V
#26 invokestatic com/company/LambdaTest.lambda$main$0:()V
#25 ()V
inDy 所需要的數(shù)據(jù)大概就是這些,可參考 Java8學(xué)習(xí)筆記(2) -- InvokeDynamic指令 - CSDN博客
inDy 運(yùn)行時(shí)
每一個(gè) inDy 指令都稱為 Dynamic Call Site(動(dòng)態(tài)調(diào)用點(diǎn)),根據(jù) jvm 規(guī)范所說(shuō)的,inDy 可以分為兩步,這兩步部分代碼代碼是在 java 層的,給 metafactory 方法設(shè)斷點(diǎn)可以看到一些行為。
第一步 inDy 需要一個(gè) CallSite(調(diào)用點(diǎn)對(duì)象),CallSite 是由 BSM 返回的,所以這一步就是調(diào)用 BSM 方法。代碼可參考:java.lang.invoke.CallSite#makeSite
調(diào)用 BSM 方法可以看作 invokevirtual 指令執(zhí)行一個(gè) invoke 方法,方法簽名如下:
invoke:(MethodHandle,Lookup,String,MethodType,/*其他附加靜態(tài)參數(shù)*/)CallSite
前四個(gè)參數(shù)是固定的,被依次壓入操作棧里
- MethodHandle,實(shí)際上這個(gè)方法句柄就是指向 BSM
- Lookup, 也就是調(diào)用者,是 Indy 指令所在的類的上下文,可以通過(guò)
Lookup#lookupClass()獲取這個(gè)類 - name ,lambda 所實(shí)現(xiàn)的方法名,也就是
"run" - invokedType,調(diào)用點(diǎn)的方法簽名,這里是
methodType(Runnable.class,String[].class)
接下來(lái)就是附加參數(shù),這些參數(shù)是靈活的,由Bootstrap methods 表提供,這里分別是:
- samMethodType,其實(shí)就是 Runnable.run 的描述符:
methodType(void.class)。sam 就 single public abstract method 的縮寫(xiě) - implMethod: 編譯器給生成的 desugar 方法,是一個(gè) MethodHandle:
caller.findStatic(LambdaTest.class,"lambda$main$0",methodType(void.class)) - instantiatedMethodType: Runnable.run 運(yùn)行時(shí)的描述符,如果方法泛型的,那這個(gè)類型可能不一樣。這里是
methodType(void.class)
上面說(shuō)的固定其實(shí)應(yīng)該是指 inDy 傳遞的實(shí)參類型是固定的,BSM 形參聲明可以是隨意,保證 BSM 能被調(diào)用就行,比如說(shuō) Lookup 聲明為 Object 不影響調(diào)用。
接下來(lái)就是執(zhí)行 LambdaMetafactory.metafactory 方法了,它會(huì)創(chuàng)建一個(gè)匿名類,這個(gè)類是通過(guò) ASM 編織字節(jié)碼在內(nèi)存中生成的,然后直接通過(guò) unsafe 直接加載而不會(huì)寫(xiě)到文件里。不過(guò)可以通過(guò)下面的虛擬機(jī)參數(shù)讓它運(yùn)行的時(shí)候輸出到文件
-Djdk.internal.lambda.dumpProxyClasses=<path>
這個(gè)類是根據(jù) lambda 的特點(diǎn)生成的,輸出后可以看到,在這個(gè)例子中是這樣的:
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);
}
}
然后就是創(chuàng)建一個(gè) CallSite,綁定一個(gè) MethodHandle,指向的方法其實(shí)就是生成的類中的靜態(tài)方法 LambdaTest$$Lambda$1.get$Lambda(String[])Runnable。然后把調(diào)用點(diǎn)對(duì)象返回,到這里 BSM 方法執(zhí)行完畢。
更詳細(xì)的可參考:
- 淺談Lambda Expression - 簡(jiǎn)書(shū)
- [Java] 關(guān)于OpenJDK對(duì)Java 8 lambda表達(dá)式的運(yùn)行時(shí)實(shí)現(xiàn)的查看方式 - 知乎專欄
第二步,就是執(zhí)行這個(gè)方法句柄了,這個(gè)過(guò)程就像 invokevirtual 指令執(zhí)行 MethodHandle#invokeExact 一樣,
加上 inDy 上面那一條 aload_0 指令,這是操作數(shù)棧有兩個(gè)分別是:
- args[],lambda 里面調(diào)用了 main 方法的參數(shù)
- 調(diào)用點(diǎn)對(duì)象(CallSite),實(shí)際上是方法句柄。如果是 CostantCallSite 的時(shí)候,inDy 會(huì)直接跟他的方法句柄鏈接。見(jiàn)代碼:MethodHandleNatives.java#L255
傳入 args,執(zhí)行方法,返回一個(gè) Runnable 對(duì)象,壓入棧頂。到這里 inDy 就執(zhí)行完畢。
接下來(lái)的指令就很好理解,astore_1 把棧頂?shù)?Runnable 對(duì)象放到局部變量表的槽位1,也是變量 r。剩下的就是再拿出來(lái)調(diào)用 run 方法。
Groovy
接下來(lái)看一下 groovy 是如何使用 inDy 指令的。先復(fù)習(xí)一遍 groovy 的方法派發(fā)。
每當(dāng) Groovy 調(diào)用一個(gè)方法時(shí),它不會(huì)直接調(diào)用它,而是要求一個(gè)中間層來(lái)代替它。 中間層通過(guò)鉤子方法允許我們更改方法調(diào)用的行為。這個(gè)中間層就是 MOP(meta object proctol),MOP 主要承載的類就是 MetaClass 。一個(gè)簡(jiǎn)化版的 MOP 主要有這些方法:
invokeMethod(String methodName, Object args)methodMissing(String name, Object arguments)getProperty(String propertyName)setProperty(String propertyName, Object newValue)propertyMissing(String name)
可以大致認(rèn)為在 Groovy 中的每個(gè)方法和屬性訪問(wèn)調(diào)用都會(huì)轉(zhuǎn)化上面的方法調(diào)用。而這些方法可以在運(yùn)行時(shí)通過(guò)重寫(xiě)修改它的默認(rèn)行為,MOP 作為方法派發(fā)的中心樞紐為 Groovy 提供了非常靈活的動(dòng)態(tài)編程的能力。
現(xiàn)在來(lái)看一下一段簡(jiǎn)短的 groovy 代碼,
class Test{
int a = 0;
static void main(args){
Test wtf = new Test()
wtf.a
wtf.doSomething()
}
}
通過(guò) groovyc -indy Test.groovy 把它編譯成字節(jié)碼。 indy 選項(xiàng)的意思就是啟用 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 指令,包括構(gòu)造函數(shù),訪問(wèn)成員變量,和不存在的方法調(diào)用都是 通過(guò) invokedynamic 實(shí)現(xiàn)的。
再看一下引導(dǎo)方法表
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
可以發(fā)現(xiàn)所有 inDy 指令的引導(dǎo)方法都是 IndyInterface.bootstrap
以方法調(diào)用的 inDy 指令為例,它的方法名稱是 "invoke",方法簽名是 methodType(Object.class,Test.class),BSM 方法還附帶兩個(gè)參數(shù)分別是實(shí)際的方法名:"doSomething" 和一個(gè)標(biāo)志:0
BSM 方法最終調(diào)用的是 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 表達(dá)式用的是 ConstantCallSite
MethodHandle mh = makeFallBack(mc,caller.lookupClass(),name,callID,type,safe,thisCall,spreadCall);
mc.setTarget(mh);
return mc;
}
主要的代碼是調(diào)用 makeFallBack 來(lái)獲取一個(gè)臨時(shí)的 MethodHandle。因?yàn)樵诘谝徊?groovy 無(wú)法確定接收者(receiver),也是就是 invoke 方法的第一個(gè)實(shí)參(Test 實(shí)例),必須要在第二步確定 CallSite 后才會(huì)傳遞過(guò)來(lái)。所以方法解析要放在第二步。
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;
}
這個(gè) fallback 方法其實(shí)就是 selectMethod。insertArguments 在這里主要做了一個(gè)柯里化的操作,因?yàn)?code>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)。
所以得經(jīng)過(guò) insertArguments 的變換,把確定的值填充進(jìn)去,用最后的數(shù)組參數(shù)來(lái)接收 inDy 傳遞的參數(shù)。這樣這個(gè)方法就能夠被 inDy 調(diào)用了。第一步創(chuàng)建 CallSite 到這里就結(jié)束。
第二步,就是 selectMethod 方法的調(diào)用,這時(shí)候 groovy 已經(jīng)知道方法的接收者 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);
}
首先創(chuàng)建一個(gè)方法解析器,在這里是 MethodSelector。接著調(diào)用 setCallSiteTarget(),這個(gè)方法就是用來(lái)解析實(shí)際的方法。具體的過(guò)程還是很復(fù)雜的,所以也沒(méi)法說(shuō)清楚,大體來(lái)說(shuō)就是確定接收者的 MetaClass,決定這個(gè)方法是實(shí)際的方法,還是交給 MetaClass 的鉤子方法,然后就是創(chuàng)建這個(gè)方法的 MethodHandle,然后把這個(gè) MethodHandle 的簽名轉(zhuǎn)化為要求的簽名。這時(shí) selecor.handle 就是最終調(diào)用的方法句柄了。接下來(lái)就是最終的方法調(diào)用了,到這里 inDy 指令就執(zhí)行完畢了。
還有一個(gè)方法值得留意:
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 的原因,如果編譯器認(rèn)為這個(gè)方法是可以緩存,那么就會(huì)把這個(gè) CallSite 綁定到實(shí)際的 MethodHandle,后續(xù)的調(diào)用就不用再重新解析了。
最后
沒(méi)有相關(guān)經(jīng)驗(yàn),inDy 還是很不好理解的,學(xué)習(xí)了 java 8 和 groovy 對(duì) inDy 的應(yīng)用才有一點(diǎn)大致的認(rèn)識(shí),文中如果有什么錯(cuò)誤,還請(qǐng)幫忙指出。
原文鏈接:https://dourok.info/2017/10/08/understanding-invokedynamic/