Java語言的動態性-invokedynamic
概述
Invokedynamic指令在JAVA7中就已經提供了,在java7之前,JVM字節碼提供了如下4種字節碼方法調用指令:
1、 Invokevirtual:根據虛方法表調用虛方法。
2、 invokespecial,:調用實例構造方法(
方法),私有方法,父類繼承方法。
3、 invokeinteface:調用接口方法。
4、 invokestatic:調用靜態方法
JVM字節碼指令集一直比較穩定,一直到JAVA7中才增加了一個invokedynamic指令,這是JAVA為了實現『動態類型語言』支持而做的一種改進。但是在JAVA7中并沒有提供直接生成invokedynamic指令的方法,需要借助ASM這種底層字節碼工具來產生invokedynamic指令。直到JAVA8的Lambda表達式的出現,invokedynamic指令的生成,在java中才有了直接的生成方式。下面詳細介紹java7之后對動態類型語言的支持。
動態類型語言和靜態類型語言
動態類型語言和靜態類型語言兩者的區別就在于對類型的檢查是在編譯期還是在運行期,滿足前者就是靜態類型語言,反之是動態類型語言。說的在直白一點就是靜態類型語言是判斷變量自身的類型信息;動態類型語言是判斷變量值的類型信息,變量沒有類型信息,變量值才有類型信息,這是動態語言的一個重要特征。
Java7中增加的動態語言類型支持的本質是對java虛擬機規范的修改,而不是對JAVA語言規則的修改,這一塊相對來講比較復雜,增加了虛擬機中的方法調用,最直接的受益者就是運行在JAVA平臺的動態語言的編譯器。
動態類型語言的支持對應的JSR 292,主要包括兩部份,一個是JAVA標準庫中的新的方法調用API,另一個是JAVA虛擬機規范中新增加的invokedynamic指令。
普通調用指令
前面概述部份提到了java7之前的4種方法調用指令,下面結合實際例子來看一下字節碼中的調用指令:
public interface InvokeInterface {
void invokeInterface(); }
public class InvokeInterfaceImpl implements InvokeInterface{
public void invokeInterface(){};
public void invokeNormalMethod(){};
public static void invokeStaticMethod(){};</code></pre>
}
public class JavaCommonInvokeInstruction {
public void invoke(){
//invokespeicial
InvokeInterface sample = new InvokeInterfaceImpl();
//invokeinterface
sample.invokeInterface();
InvokeInterfaceImpl sampleImpl = new InvokeInterfaceImpl();
//invokevirtual
sampleImpl.invokeNormalMethod();
//invokestatic
InvokeInterfaceImpl.invokeStaticMethod();
}
} 編譯后,查看JavaCommonInvokeInstruction#invoke()字節碼 javap –v JavaCommonInvokeInstruction 下圖截取了invoke的字節調用碼,不包括局部變量表等其它信息。也省略了類的其它信息,如常量池等。
圖1 上圖中,1-5的序號就是java中4種普通方法調用指令。其中1和3是同樣的,都是對象的構造方法調用(私有方法調用,父類方法調用也是這個指令)。 2處是接口方法調用指令。4處是普通方法的調用,這個指令用于調用類中的一般方法。5處是類的靜態方法調用。 除了invokestatic指令外,其它三個方法調用指令都有一個接收對像。或者說invokestatic的接收者是一個類。對于另外三種指令的接收者來說,都有一個在靜太類型,也就是編譯期確定的對象類型,對于invokespecial和invokevirtual來說靜態類型就是接收者的對象類型。對于invokeinterface來說,靜態類型就是接口的類型。
存在編譯期確定的靜態類型,那同樣存在運行期才確認的動態類型。動態類型可能和靜態類型相同,也可能不同;如果不同,則動態類型肯定是靜態類型的子類型,否則字節碼校驗時會不通過。
方法分派#
靜態類型和動態類型又和java的方法分派有著密不可分的關系。方法的分派根據方法版本確定的時機分為靜態分派,動態分派。
我們知道,java中所有方法的調用在class文件中都對應一個常量池中的符號引用,在類加載的解析階段,符號引用會被解析為直接引用,有一類方法調用在運行之前就可確定一個唯一版本,這個版本在運行時不可變,我們稱這類方法調用為解析,對應的方法調用字節碼為:invokespecial和invokestatic。靜態方法,構造方法,私有方法以及父類方法(通過super調用的方法)都無法通過繼承的方式被覆蓋,因為在編譯期就能確定其版本。我們稱這類分派為靜態分派,方法的重載是這種類型的典型場景。
另一種分派為動態分派,故名思義,是在編譯期無法確定其接收者真實類型,編譯期只能確定方法接收者的靜態類型,根據靜態類型確定所調用方法的簽名,無法知道其運行期值的類型,具體調用哪個類的相應方法版本只有在運行期根據接收者的實際類型來確定。方法的重寫是這種應用的典型場景。對應的方法調用字節碼為:invokevirtual和invokeinterface。
《深入java虛擬機》一收中提到,方法的分配根據宗量(影響最終方法確認的變量種數,如參數,接收者實際類型等)數的多少,分派又分為單分派和多分派。Java在編譯期根據方法接收者的靜態類型和方法實際參數類型確定方法的最終簽名,所以java是一種靜態多分派語言。同時,方法的簽名一旦在編譯期確定之后,運行期不再關心傳入參數的類型,而只關心接收者的實際類型,所以運行期景響方法調用的宗量只有一個接收者實際值類型,所以java是一種動態單分派語言。
以上描述大家可以通過重載和覆蓋來詳細理解。另外,上述狀態僅在java7之前版本存在。Java7之后增加了jvm對動態類型語言的支持,使java完全支持了動態多分派特性。Java7之后從虛擬機層面直接加入了對動態類型語言的支持,這就是大名鼎鼎的invokedynamic和java.lang.invoke包。
Java.lang.invoke包
JSR-292是JVM為動態類型支持而出現的規范,在JAVA7中實現了這個規則,這個包的主要作用就在之前只能依賴符號引用來確定目標方法的基礎上,增加了一種動態確定目標方法的機制,也就是方法句柄MethodHandler。這有點類似于C++中的函數指針。從功能上講,方法句柄類似于反射中的Method類,但兩者之間有區別,方法句柄是輕量級的,我們從Method和MethodHandler的實現上可以看出來,Method的invoke方法會涉及到JAVA的安全訪問檢查,而方法句柄的所有invoke方法都是native方法,其性能優于反射(后面會給出性能對比示例);有一點相同,反射和方法句柄都是通過JAVA7之前的4種invoke方法調用指令實現的。
反射
如下例: public class ReflectionMain {
public void reflection() throws Exception { String methodName = "length"; Method method = String.class.getDeclaredMethod(methodName); Object result = method.invoke("abc"); }
} 上述java類模擬調用string類的length方法,其對應的字節碼如下,未列出所有字節碼內容,只列出來了方法的對應的字節碼。
圖2
方法句柄
下面給出一個方法句柄的基礎使用示例,可以對比和反射的區別: public class InvokeExact { public void invokeExact() throws Throwable { MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodType type = MethodType.methodType(String.class, int.class, int.class); MethodHandle mh = lookup.findVirtual(String.class, "substring", type); String str = (String) mh.invokeExact("Hello World", 1, 3); } } 可以看出方法句柄的使用與方法類型要配合。Invoke包把方法類型使用MethodType描述,此對象主要表示方法的簽名。MethodHandler和MethodType的組合可以更靈活多變。下面是上述類對應的字節碼,本質上與反射沒區別:
圖3
方法類型
MethodType提供了多種工廠方法來獲取實例,主要使用methodType方法,這個方法有多種形式的重載,它主要是通過返回值類型和參數類型來創建MethodType實例。其最少要有一個參數,表示方法返回值,參數可以有0個。methodType方法第一個參數表示返回值。
MethodType類提供了一個比較特殊的工廠方法:fromMethodDescriptorString,這個方法允許使用JAVA字節碼中的類型描述字符串來創建MethodType,這種方法適合對字節碼的類型描述比較熟悉的開發人員。如下例:
` public class MethodTypeFromMethodDescriptor {
public void generateMethodTypesFromDescriptor() {
ClassLoader cl = this.getClass().getClassLoader();
String descriptor = "(Ljava/lang/String;)Ljava/lang/String;";
MethodType mt1 = MethodType.fromMethodDescriptorString(descriptor, cl);
System.out.println(mt1);
}
` 獲取MethodType實例后,可以對其進行修改,修改后得到一個新的MethodType實例,修改包括修改返回值,插入刪除參數,修改已有參數等。還可以在基本類型和包裝類型之間轉換。不作詳細描述。
方法句柄應用
方法調用
參考前面InvokeExact類的invokeExact方法,此方法使用了方法句柄的invokeExact方法去調用底層方法。此方法有比較嚴格的限制,其返回類型要和MethodType的返回值嚴格相同,也就是方法調用前的(String)強制類型轉換不可省略,即使不賦值給一個變量,省略了強制類型,JVM會認為其返回值類型為void,這也和MethodType不匹配的。詳細情況,大家可以測試一下。
相反,invoke方法沒有這么嚴格要限制,基會根據方法句柄調用時的方法類型(這個是動態的)和其聲明時的方法類型作類型轉換,如果不在轉換規則內,方法調用會失敗。
可變參數長度的方法句柄
1、asVarargsCollector方法,它的作用是把原始方法句柄對應的方法類型的最后一個數組類型的參數轉換成對應類型的可變長度參數,如下例:
public class Varargs {
public void normalMethod(String arg1, int arg2, int[] arg3) {
}
public void asVarargsCollector() throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle mh = lookup.findVirtual(Varargs.class, "normalMethod", MethodType.methodType(void.class, String.class, int.class, int[].class));
mh = mh.asVarargsCollector(int[].class);
mh.invoke(this, "Hello", 2, 3, 4, 5);
System.out.println(mh.type());
}</code></pre>
} 2、asCollector方法,作用與asVarargsCollector類似,不同的是該方法只會把指定數量的參數收集到原始方法句柄對應的底層方法的數組類型參數中。如:
public void asCollector() throws Throwable { MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodHandle mh = lookup.findVirtual(Varargs.class, "normalMethod", MethodType.methodType(void.class, String.class, int.class, int[].class)); mh = mh.asCollector(int[].class, 2); mh.invoke(this, "Hello", 2, 3, 4); System.out.println(mh.type()); } 3、asSpreader方法,與上述兩個方法的轉換方向相返,上述兩個方法是把數組類型的參數轉換成長度可變的數組,而asSpreader正好相反。如下例:
public void toBeSpreaded(String arg1, int arg2, int arg3, int arg4) {
}
public void asSpreader() throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle mh = lookup.findVirtual(Varargs.class, "toBeSpreaded", MethodType.methodType(void.class, String.class, int.class, int.class, int.class));
mh = mh.asSpreader(int[].class, 3);
mh.invoke(this, "Hello", new int[]{3, 4, 5});
System.out.println(mh.type());
}</code></pre>
4、asFixedArity方法,把參數長度可變的方法,轉換為參數長度不可變的方法。如:
public void varargsMethod(String arg1, int... args) {
}
public void asFixedArity() throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle mh = lookup.findVirtual(Varargs.class, "varargsMethod", MethodType.methodType(void.class, String.class, int[].class));
mh = mh.asFixedArity();
mh.invoke(this, "Hello", new int[]{2, 4});
System.out.println(mh.type());
}</code></pre>
參數綁定
如前示例所示,如果方法句柄調用的非靜態方法,則在調用時要提供一個方法接收者。這個接收者可以在invoke時同時指定,也可以通過bindTo方法指定。另外一點要注意的,bindTo只是綁定方法句柄的第一個參數,bindTo可以多次調用,依次綁定相應的參數。如:
public void multipleBindTo() throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle mh = lookup.findVirtual(String.class, "indexOf",
MethodType.methodType(int.class, String.class, int.class));
mh = mh.bindTo("Hello").bindTo("l");
System.out.println(mh.invoke(2));
</code></pre>
獲取方法句柄
前面的示例都是通過MethodHandles.Lookup查找類來獲取方法句柄的,除了MethodType抽象出來了之外,其查找底層方法與反射也基本類似,但不同的是,方法句柄并不區分構造方法,方法,域等,而是統一轉換成MethodHandle。Lookup提供了不同的查找方法,如下例:
public void lookupMethod() throws NoSuchMethodException, IllegalAccessException { MethodHandles.Lookup lookup = MethodHandles.lookup(); //構造方法 lookup.findConstructor(String.class, MethodType.methodType(void.class, byte[].class)); //String.substring lookup.findVirtual(String.class, "substring", MethodType.methodType(String.class, int.class, int.class)); //String.format lookup.findStatic(String.class, "format", MethodType.methodType(String.class, String.class, Object[].class)); } 另外,Lookup也提供查找私有方法的工廠方法,findSpecial,調用此方法獲取底層私有方法的句柄時,要符合JVM的訪問控制要求,進行方法查找的類要具備訪問私有方法的權限,也就是說方法句柄是在查找方法時進行訪問控制校驗的,而不是像反射是在執行時。另外,findSpecial方法參數也比之前幾種多一個,這個參數用來指定私有方法被調用時所使用的類,這個類要有對這個私有方法的訪問權限,否則將出錯。
public MethodHandle lookupSpecial() throws NoSuchMethodException, IllegalAccessException, Throwable { MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodHandle mh = lookup.findSpecial(MethodHandleLookup.class, "protectedMethod", MethodType.methodType(void.class), MethodHandleLookup.class); System.out.println(mh.type()); mh.invoke(this); return mh; } 另個Lookup還提供了一些setter、getter方法的查找,還可以通過反射API得到的構造器、方法、域等信息查找相應的方法句柄,同學們可以自行研究。
其它
方法句柄還有其它很多靈活的使用方法,有待大家發掘,如:方法句柄變換,MethodHands提供了豐富的api可以對原方法句柄進行相應的變換。如:dropArguments,insertArguments,filterAgrements等,還有很多。大家可以自行研究。 MethodHand的invoke方法,有一個版本可以傳遞另一個方法句柄作為參數,通過此功能可以完成責任鏈的處理模式。甚至功能更強。
方法句柄還可以實接口、實現函數式編程等。所有這些功能大家可以自己研究一下。
方法句柄與反射的性能對比
public class ContrastMethodHandleAndRelection {
final static Long COUNT = 100000L;
//-XX:CompileThreshold=500 -XX:+PrintCompilation
public static Object reflection(Method method) throws Exception {
int result = (int)method.invoke("abc");
return Math.multiplyExact(result,Long.BYTES);
}
public static Object methodHandle(MethodHandle mh) throws Throwable{
int result = (int) mh.invoke("abc");
return Math.multiplyExact(result,Long.BYTES);
}
public static void main(String[] args) throws Throwable{
String methodName = "length";
Method method = String.class.getDeclaredMethod(methodName);
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType type = MethodType.methodType(int.class);
MethodHandle mh = lookup.findVirtual(String.class, methodName, type);
//預熱
for(int i=0 ;i<COUNT; i++){
reflection(method);
methodHandle(mh);
}
long time = System.currentTimeMillis();
for(int i=0 ;i<COUNT; i++){
reflection(method);
}
System.out.println(COUNT +" times Reflection spent ["+(System.currentTimeMillis()-time)+"]ms");
time = System.currentTimeMillis();
for(int i=0 ;i<COUNT; i++){
methodHandle(mh);
}
System.out.println(COUNT +" times MethodHandle spent ["+(System.currentTimeMillis()-time)+"]ms");
}</code></pre>
} 上述示例分別設置JVM參數-Xint以解釋執行方式,和-server,默認JIT閾值為10000,運行看結果,經果發現解釋執行的結果方法句柄方式比反射快將近2倍,以JIT方法運行,反射也略比方法句柄方式慢一點。不明顯。
java8中的invokedynamic
java7中提供了invokedynamic指令,更多的是為動態語言提供一種運行機制,java7本身并沒有直接提供相應的方法生成invokedynamic字節碼,需要借助asm這個字節碼框架結合bootstrap和CallSite來間接實現,這里不介紹詳細實現方式,有興趣的同學baidu一下吧。下面介紹一個java8中怎么使用invokedynamic指令。
@FunctionalInterface interface Func {
public boolean func(String str);
}
public class Lambda {
public void lambda(Func func){
return;
}
public static void main(String[] args) {
Lambda lambda = new Lambda();
lambda.lambda(s->{return true;});
lambda.lambda(s->{return true;});
}
}</code></pre>
上述示例字義了一個函數接口,通過@FunctionalInterface注解標注,此接口的特征是只能一個接口方法,可以包括0個或多個default接口方法。用于Lambda表達式的接口有且只能有一個接口方法。下面看一下Lambda編譯之后對應的字節碼。 
圖4
圖4中1和2處對應Lambda類中兩處lambda方法的調用中的lambda表達式。可以看出,編譯器對每一處的lambda表達式的調用都會生成一個invokedynamci指令。其對應的#4和#6常量池如下:
圖5
忽略#5,4和6號常量池對應的類型為InvokeDynamic,其后的#0和#1,代表的是BootstrapMethods屬性表中的第0項和第1項。下面為這BootstrapMethods屬性表的內容:
圖6
我們以第0項為例介紹,第1項跟0項一樣。 跟第0項相關的常量池編號為#33,#34和#35,內容如下:
圖7
其中33是invokedynamic指令所指定的bootstrap方法,編譯器置入,java7中要自己提供一個這種靜態方法,由asm工具寫入字節碼中,java8中jdk提供了這樣的一個啟動方法。JVM在類加載解析時,如果是invokedynamic時,每次都會進行重新解析,解析的時候,會首先執行bootstrap方法,LambdaMetafactory.metafactory方法的前三個參數,會在運行時根據訪問類和運行期常量池動態傳入,而后三個參數,則為bootstrap靜態參數列表傳入。 34和35一個是方法類型參數,一個是方法句柄對象,這兩個分別作為bootstrap方法的參數傳入到LambdaMetafactory.metafactory方法中。感興趣的可以debug看一下。
順著#34繼續查看,其常量類型為MethodType,發現其對應的#24為UTF8類型,這個就是方法類型的描述,對應到源碼中就是Func.func(String s)方法,只是這個方法類型描述僅有參數和返回值,不包括方法名。 查看#35常量池,其對應的是invokestatic指令,35號常量池內容如下: 
圖8
根據java字節碼規范,CONSTANT MethodHandle info常量池的結構包括:referent_kind,這個代表方法句柄的類型,范圍必須為1-9,字由方法句柄的字節碼行為決定。圖7中其取值為6(invokestatic),剩下的8種有興趣的可以查一下字節碼規范,它們分別應對不同的方法句柄類型,如構造器等。#35號常量池引用#6和#41號常量池:
圖9
圖10
我們發現在圖4中第2處也是引用的#6常量池,只不過對應的指令為invokedynamic,而圖8的#35和#38常量池后面也有一個#6的參數,這個參數并不表示每6個常量池,而是CONSTRANT MethodHandle info的reference_kind的值為6,表示的是invokestatic指令圖4的#6常量池的#1引用的是bootstrapMethods屬性表中的第1項。 使用javap –v –p Lambda.class查看字節碼,發現其中有兩個靜態的private的方法,如下:
圖11
這兩個方法就是編譯器對lambda表達式生成的相應方法,另外,這兩個方法都是private的,也就是只能在此類內部訪問。我們知道,方法句柄有一個啟動方法,這個方法返回一個調用點,這個調用點引用一個MethodHandler,也就是說最終對這兩個private類型的lambda方法的調用在Lambda類之外,這違反了jvm安全機制。方法句柄在調用類的私有或保護類型方法時,要傳入一個有訪問權限的類,由此驗證上面的猜測。這個類的實例就是:java/lang/invoke/LambdaMetafactory.metafactory這個方法的第一個MethodHandles.Lookup參數對應的lookupClass. 在此例中,這個類就是Lambda類。圖12是Lambda進行debug時,LambdaMetafactory.metafactory方法的運行時參數值,我們可以觀察到其參數值:
圖12
來自:https://blog.souche.com/javayu-yan-de-dong-tai-xing-invokedynamic/