Java虛擬機的字節(jié)碼指令集的數(shù)量從Sun公司的第一款Java虛擬機問世至JDK 7來臨之前的十余年時間里寡键,一直沒有發(fā)生任何變化。隨著JDK 7的發(fā)布雪隧,字節(jié)碼指令集終于迎來了第一位新成員——invokedynamic指令。這條新增加的指令是JDK 7實現(xiàn)“動態(tài)類型語言”(Dynamically Typed Language)支持而進(jìn)行的改進(jìn)之一员舵,也是為JDK 8可以順利實現(xiàn)Lambda表達(dá)式做技術(shù)準(zhǔn)備脑沿。
分派
- 方法調(diào)用階段唯一的任務(wù)為確認(rèn)調(diào)用的版本是哪個
- 字節(jié)碼編譯的過程,連接的部分都是符號引用马僻,調(diào)用可能分別在 編譯器庄拇、類加載期甚至運行期才能確定目標(biāo)方法
- 靜態(tài)類型:申明的類型;動態(tài)類型:實際的類型
- 靜態(tài)重載往往是確定一個相對更合適的版本韭邓,可能會導(dǎo)致引用模糊的錯誤
- 編譯器(類加載期)可知措近,靜態(tài)多分派(靜態(tài)類型+方法簽名)
- invokestatic:用于調(diào)用靜態(tài)方法
- invokespecial:用于調(diào)用實例構(gòu)造器<init>()方法、私有方法和父類中的方法
- invokeinterface:用于調(diào)用接口方法女淑,會在運行時再確定一個實現(xiàn)該接口的對象
- invokedynamic:先在運行時動態(tài)解析出調(diào)用點限定符所引用的方法瞭郑,然后再執(zhí)行該方法 。分派邏輯是由用戶設(shè)定的引 導(dǎo)方法來決定的
- 運行期才可知鸭你,動態(tài)單分派(方法接收者的實際類型)
- invokevirtual:用于調(diào)用所有的虛方法
動態(tài)類型語言
動態(tài)類型語言是什么屈张?它與Java語言擒权、Java虛擬機有什么關(guān)系?
什么是動態(tài)類型語言阁谆?注意碳抄,動態(tài)類型語言與動態(tài)語言、弱類型語言并不是一個概念场绿,需要區(qū)別對待剖效。
動態(tài)類型語言的關(guān)鍵特征是它的類型檢查的主體過程是在運行期而不是編譯期,滿足這個特征的語言有很多焰盗,常用的包括:APL璧尸、Clojure、Erlang姨谷、Groovy逗宁、JavaScript、Jython梦湘、Lisp瞎颗、Lua、PHP捌议、Prolog哼拔、Python、Ruby瓣颅、Smalltalk和Tcl等倦逐。相對的,在編譯期就進(jìn)行類型檢查過程的語言(如C++和Java等)就是最常用的靜態(tài)類型語言宫补。
覺得上面定義過于概念化檬姥?那我們不妨通過兩個例子以最淺顯的方式來說明什么是“在編譯期/運行期進(jìn)行”和什么是“類型檢查”。
首先看下面這段簡單的Java代碼粉怕,它是否能正常編譯和運行健民?---在編譯期/運行期進(jìn)行
public static void main(String[] args){
int[][][] array=new int[1][0][-1];
}
這段代碼能夠正常編譯,但運行的時候會報NegativeArraySizeException異常贫贝。在Java虛擬機規(guī)范中明確規(guī)定了NegativeArraySizeException是一個運行時異常秉犹,通俗一點來說,運行時異常就是只要代碼不運行到這一行就不會有問題稚晚。與運行時異常相對應(yīng)的是連接時異常崇堵,例如很常見的NoClassDefFoundError便屬于連接時異常,即使會導(dǎo)致連接時異常的代碼放在一條無法執(zhí)行到的分支路徑上客燕,類加載時(Java的連接過程不在編譯階段鸳劳,而在類加載階段)也照樣會拋出異常。
例如下面這一句非常簡單的代碼:----類型檢查
obj.println("hello world")幸逆;
雖然每個人都能看懂這行代碼要做什么棍辕,但對于計算機來說暮现,這一行代碼“沒頭沒尾”是無法執(zhí)行的,它需要一個具體的上下文才有討論的意義楚昭。
現(xiàn)在假設(shè)這行代碼是在Java語言中栖袋,并且變量obj的靜態(tài)類型為java.io.PrintStream,那變量obj的實際類型就必須是PrintStream的子類(實現(xiàn)了PrintStream接口的類)才是合法的抚太。否則塘幅,哪怕obj屬于一個確實有用println(String)方法,但與PrintStream接口沒有繼承關(guān)系尿贫,代碼依然不可能運行——因為類型檢查不合法电媳。
但是相同的代碼在ECMAScript(JavaScript)中情況則不一樣,無論obj具體是何種類型庆亡,只要這種類型的定義中確實包含有println(String)方法匾乓,那方法調(diào)用便可成功。
這種差別產(chǎn)生的原因是Java語言在編譯期間已將println(String)方法完整的符號引用(本例中為一個CONSTANT_InterfaceMethodref_info常量)生成出來又谋,作為方法調(diào)用指令的參數(shù)存儲到Class文件中拼缝,例如下面這段代碼:
invokevirtual#4;//Method java/io/PrintStream.println:(Ljava/lang/String彰亥;)V
這個符號引用包含了此方法定義在哪個具體類型之中咧七、方法的名字以及參數(shù)順序、參數(shù)類型和方法返回值等信息任斋,通過這個符號引用继阻,虛擬機可以翻譯出這個方法的直接引用。而在ECMAScript等動態(tài)類型語言中废酷,變量obj本身是沒有類型的瘟檩,變量obj的值才具有類型,編譯時最多只能確定方法名稱澈蟆、參數(shù)芒帕、返回值這些信息,而不會去確定方法所在的具體類型(即方法接收者不固定)丰介。“變量無類型而變量值才有類型”這個特點也是動態(tài)類型語言的一個重要特征鉴分。
JDK 1.7與動態(tài)類型
Java虛擬機毫無疑問是Java語言的運行平臺哮幢,但它的使命并不僅限于此,早在1997年出版的《Java虛擬機規(guī)范》中就規(guī)劃了這樣一個愿景:“在未來志珍,我們會對Java虛擬機進(jìn)行適當(dāng)?shù)臄U展橙垢,以便更好地支持其他語言運行于Java虛擬機之上”。而目前確實已經(jīng)有許多動態(tài)類型語言運行于Java虛擬機之上了伦糯,如Clojure柜某、Groovy嗽元、Jython和JRuby等,能夠在同一個虛擬機上可以達(dá)到靜態(tài)類型語言的嚴(yán)謹(jǐn)性與動態(tài)類型語言的靈活性喂击,這是一件很美妙的事情剂癌。
但遺憾的是,Java虛擬機層面對動態(tài)類型語言的支持一直都有所欠缺翰绊,主要表現(xiàn)在方法調(diào)用方面:JDK 1.7以前的字節(jié)碼指令集中佩谷,4條方法調(diào)用指令(invokevirtual、invokespecial监嗜、invokestatic谐檀、invokeinterface)的第一個參數(shù)都是被調(diào)用的方法的符號引用(CONSTANT_Methodref_info或者CONSTANT_InterfaceMethodref_info常量),方法的符號引用在編譯時產(chǎn)生裁奇,而動態(tài)類型語言只有在運行期才能確定接收者類型桐猬。這樣,在Java虛擬機上實現(xiàn)的動態(tài)類型語言就不得不使用其他方式(如編譯時留個占位符類型刽肠,運行時動態(tài)生成字節(jié)碼實現(xiàn)具體類型到占位符類型的適配)來實現(xiàn)溃肪,這樣勢必讓動態(tài)類型語言實現(xiàn)的復(fù)雜度增加,也可能帶來額外的性能或者內(nèi)存開銷五垮。盡管可以利用一些辦法(如Call Site Caching)讓這些開銷盡量變小乍惊,但這種底層問題終歸是應(yīng)當(dāng)在虛擬機層次上去解決才最合適,因此在Java虛擬機層面上提供動態(tài)類型的直接支持就成為了Java平臺的發(fā)展趨勢之一放仗,這就是JDK 1.7(JSR-292)中invokedynamic指令以及java.lang.invoke包出現(xiàn)的技術(shù)背景润绎。
java.lang.invoke包
JDK 1.7實現(xiàn)了JSR-292,新加入的java.lang.invoke包诞挨。這個包的主要目的是在之前單純依靠符號引用來確定調(diào)用的目標(biāo)方法這種方式以外莉撇,提供一種新的動態(tài)確定目標(biāo)方法的機制,稱為MethodHandle惶傻。
MethodHandle的基本用途棍郎,無論obj是何種類型(臨時定義的ClassA抑或是實現(xiàn)PrintStream接口的實現(xiàn)類System.out),都可以正確地調(diào)用到println()方法银室。
import static java.lang.invoke.MethodHandles.lookup;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
public class MethodHandleTest{
static class ClassA{
public void println(String s){
System.out.println(s);
}
}
public static void main(String[] args)throws Throwable{
Object obj=System.currentTimeMillis()%2==0?System.out:new ClassA();
/*無論obj最終是哪個實現(xiàn)類涂佃,下面這句都能正確調(diào)用到println方法*/
getPrintlnMH(obj).invokeExact("icyfenix");
}
private static MethodHandle getPrintlnMH(Object reveiver) throws Throwable{
/*MethodType:代表“方法類型”,包含了方法的返回值(methodType()的第一個參數(shù))和具體參數(shù)(methodType()第二個及以后的參數(shù))*/
MethodType mt=MethodType.methodType(void.class,String.class);
/*lookup()方法來自于MethodHandles.lookup蜈敢,這句的作用是在指定類中查找符合給定的方法名稱辜荠、方法類型,并且符合調(diào)用權(quán)限的方法句柄
因為這里調(diào)用的是一個虛方法抓狭,按照J(rèn)ava語言的規(guī)則伯病,方法第一個參數(shù)是隱式的,代表該方法的接收者否过,也即是this指向的對象午笛,
這個參數(shù)以前是放在參數(shù)列表中進(jìn)行傳遞的惭蟋,而現(xiàn)在提供了bindTo()方法來完成這件事情*/
return lookup().findVirtual(reveiver.getClass(),"println",mt).bindTo(reveiver);
}
}
實際上,方法getPrintlnMH()中模擬了invokevirtual指令的執(zhí)行過程药磺,只不過它的分派邏輯并非固化在Class文件的字節(jié)碼上告组,而是通過一個具體方法來實現(xiàn)。而這個方法本身的返回值(MethodHandle對象)与涡,可以視為對最終調(diào)用方法的一個“引用”惹谐。
相同的事情,用反射不是早就可以實現(xiàn)了嗎驼卖?
僅站在Java語言的角度來看氨肌,MethodHandle的使用方法和效果與Reflection有眾多相似之處汞扎,不過逢并,它們還是有以下這些區(qū)別:
- 從本質(zhì)上講羹膳,Reflection和MethodHandle機制都是在模擬方法調(diào)用帝蒿,但Reflection是在模擬Java代碼層次的方法調(diào)用肝集,而MethodHandle是在模擬字節(jié)碼層次的方法調(diào)用野舶。在MethodHandles.lookup中的3個方法——findStatic()峦耘、findVirtual()砌们、findSpecial()正是為了對應(yīng)于invokestatic贩虾、invokevirtual&invokeinterface和invokespecial這幾條字節(jié)碼指令的執(zhí)行權(quán)限校驗行為催烘,而這些底層細(xì)節(jié)在使用Reflection API時是不需要關(guān)心的。
- Reflection中的java.lang.reflect.Method對象遠(yuǎn)比MethodHandle機制中的java.lang.invoke.MethodHandle對象所包含的信息多缎罢。前者是方法在Java一端的全面映像伊群,包含了方法的簽名、描述符以及方法屬性表中各種屬性的Java端表示方式策精,還包含執(zhí)行權(quán)限等的運行期信息舰始。而后者僅僅包含與執(zhí)行該方法相關(guān)的信息。用通俗的話來講咽袜,Reflection是重量級丸卷,而MethodHandle是輕量級。
- 由于MethodHandle是對字節(jié)碼的方法指令調(diào)用的模擬询刹,所以理論上虛擬機在這方面做的各種優(yōu)化(如方法內(nèi)聯(lián))谜嫉,在MethodHandle上也應(yīng)當(dāng)可以采用類似思路去支持(但目前實現(xiàn)還不完善)。而通過反射去調(diào)用方法則不行凹联。
- MethodHandle與Reflection除了上面列舉的區(qū)別外骄恶,最關(guān)鍵的一點還在于去掉前面討論施加的前提“僅站在Java語言的角度來看”:Reflection API的設(shè)計目標(biāo)是只為Java語言服務(wù)的,而MethodHandle則設(shè)計成可服務(wù)于所有Java虛擬機之上的語言匕垫,其中也包括Java語言。
invokedynamic指令
在某種程度上虐呻,invokedynamic指令與MethodHandle機制的作用是一樣的象泵,都是為了解決原有4條“invoke*”指令方法分派規(guī)則固化在虛擬機之中的問題寞秃,把如何查找目標(biāo)方法的決定權(quán)從虛擬機轉(zhuǎn)嫁到具體用戶代碼之中,讓用戶(包含其他語言的設(shè)計者)有更高的自由度偶惠。而且春寿,它們兩者的思路也是可類比的,可以把它們想象成為了達(dá)成同一個目的忽孽,一個采用上層Java代碼和API來實現(xiàn)绑改,另一個用字節(jié)碼和Class中其他屬性、常量來完成兄一。因此厘线,如果理解了前面的MethodHandle例子,那么理解invokedynamic指令也并不困難出革。
每一處含有invokedynamic指令的位置都稱做“動態(tài)調(diào)用點”(Dynamic Call Site)造壮,這條指令的第一個參數(shù)不再是代表方法符號引用的CONSTANT_Methodref_info常量,而是變?yōu)镴DK 1.7新加入的CONSTANT_InvokeDynamic_info常量骂束,從這個新常量中可以得到3項信息:引導(dǎo)方法(Bootstrap Method耳璧,此方法存放在新增的BootstrapMethods屬性中)、方法類型(MethodType)和名稱展箱。引導(dǎo)方法是有固定的參數(shù)旨枯,并且返回值是java.lang.invoke.CallSite對象,這個代表真正要執(zhí)行的目標(biāo)方法調(diào)用混驰。根據(jù)CONSTANT_InvokeDynamic_info常量中提供的信息攀隔,虛擬機可以找到并且執(zhí)行引導(dǎo)方法,從而獲得一個CallSite對象账胧,最終調(diào)用要執(zhí)行的目標(biāo)方法竞慢。
舉一個實際的例子來解釋這個過程,如下所示治泥。
import static java.lang.invoke.MethodHandles.lookup筹煮;
import java.lang.invoke.CallSite;
import java.lang.invoke.ConstantCallSite居夹;
import java.lang.invoke.MethodHandle败潦;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType准脂;
public class InvokeDynamicTest{
public static void main(String[]args)throws Throwable{
INDY_BootstrapMethod().invokeExact("icyfenix")劫扒;
}
public static void testMethod(String s){
System.out.println("hello String:"+s);
}
public static CallSite BootstrapMethod(MethodHandles.Lookup lookup,String name,MethodType mt)throws Throwable{
return new ConstantCallSite(lookup.findStatic(InvokeDynamicTest.class,name,mt))狸膏;
}
private static MethodType MT_BootstrapMethod(){
return MethodType.fromMethodDescriptorString("(Ljava/lang/invoke/MethodHandles $Lookup沟饥;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite贤旷;"广料,null);
}
private static MethodHandle MH_BootstrapMethod()throws Throwable{
return lookup().findStatic(InvokeDynamicTest.class幼驶,"BootstrapMethod"艾杏,MT_BootstrapMethod());
}
private static MethodHandle INDY_BootstrapMethod()throws Throwable{
CallSite cs=(CallSite)MH_BootstrapMethod().invokeWithArguments(lookup()盅藻,"testMethod"购桑,MethodType.fromMethodDescriptorString("(Ljava/lang/String;)V"氏淑,null))勃蜘;
return cs.dynamicInvoker();
}
}
看一下BootstrapMethod()夸政,所有邏輯就是調(diào)用MethodHandles$Lookup的findStatic()方法元旬,產(chǎn)生testMethod()方法的MethodHandle,然后用它創(chuàng)建一個ConstantCallSite對象守问。最后匀归,這個對象返回給invokedynamic指令實現(xiàn)對testMethod()方法的調(diào)用,invokedynamic指令的調(diào)用過程到此就宣告完成了耗帕。
掌控方法分派規(guī)則
invokedynamic指令與前面4條“invoke*”指令的最大差別就是它的分派邏輯不是由虛擬機決定的穆端,而是由程序員決定。
class GrandFather{
void thinking(){
System.out.println("i am grandfather")仿便;
}
}
class Father extends GrandFather{
void thinking(){
System.out.println("i am father")体啰;
}
}
class Son extends Father{
void thinking(){
//請在這里填入適當(dāng)?shù)拇a(不能修改其他地方的代碼)
//實現(xiàn)調(diào)用祖父類的thinking()方法,打印"i am grandfather"
}
}
在Java程序中嗽仪,可以通過“super”關(guān)鍵字很方便地調(diào)用到父類中的方法荒勇,但如果要訪問祖類的方法呢?
在JDK 1.7之前闻坚,使用純粹的Java語言很難處理這個問題(直接生成字節(jié)碼就很簡單沽翔,如使用ASM等字節(jié)碼工具),原因是在Son類的thinking()方法中無法獲取一個實際類型是GrandFather的對象引用窿凤,而invokevirtual指令的分派邏輯就是按照方法接收者的實際類型進(jìn)行分派仅偎,這個邏輯是固化在虛擬機中的,程序員無法改變雳殊。在JDK 1.7中橘沥,可以使用MethodHandle來解決相關(guān)問題。
import static java.lang.invoke.MethodHandles.lookup;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
public class Test{
class GrandFather{
void thinking(){
System.out.println("i am grandfather");
}
}
class Father extends GrandFather{
void thinking(){
System.out.println("i am father");
}
}
class Son extends Father{
void thinking(){
try{
MethodType mt=MethodType.methodType(void.class);
MethodHandle mh=lookup().findVirtual(GrandFather.class,"thinking",mt).bindTo(new Test().new GrandFather());
mh.invokeExact();
}catch(Throwable e){
}
}
}
public static void main(String[] args){
(new Test().new Son()).thinking();
}
}