invokedynamic指令

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ū)別:

  1. 從本質(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)心的。
  2. Reflection中的java.lang.reflect.Method對象遠(yuǎn)比MethodHandle機制中的java.lang.invoke.MethodHandle對象所包含的信息多缎罢。前者是方法在Java一端的全面映像伊群,包含了方法的簽名、描述符以及方法屬性表中各種屬性的Java端表示方式策精,還包含執(zhí)行權(quán)限等的運行期信息舰始。而后者僅僅包含與執(zhí)行該方法相關(guān)的信息。用通俗的話來講咽袜,Reflection是重量級丸卷,而MethodHandle是輕量級。
  3. 由于MethodHandle是對字節(jié)碼的方法指令調(diào)用的模擬询刹,所以理論上虛擬機在這方面做的各種優(yōu)化(如方法內(nèi)聯(lián))谜嫉,在MethodHandle上也應(yīng)當(dāng)可以采用類似思路去支持(但目前實現(xiàn)還不完善)。而通過反射去調(diào)用方法則不行凹联。
  4. 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();
    }
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末夯秃,一起剝皮案震驚了整個濱河市座咆,隨后出現(xiàn)的幾起案子痢艺,更是在濱河造成了極大的恐慌,老刑警劉巖介陶,帶你破解...
    沈念sama閱讀 218,036評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件腹备,死亡現(xiàn)場離奇詭異,居然都是意外死亡斤蔓,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評論 3 395
  • 文/潘曉璐 我一進(jìn)店門镀岛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來弦牡,“玉大人,你說我怎么就攤上這事漂羊〖菝蹋” “怎么了?”我有些...
    開封第一講書人閱讀 164,411評論 0 354
  • 文/不壞的土叔 我叫張陵走越,是天一觀的道長椭豫。 經(jīng)常有香客問我,道長旨指,這世上最難降的妖魔是什么赏酥? 我笑而不...
    開封第一講書人閱讀 58,622評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮谆构,結(jié)果婚禮上裸扶,老公的妹妹穿的比我還像新娘。我一直安慰自己搬素,他們只是感情好呵晨,可當(dāng)我...
    茶點故事閱讀 67,661評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著熬尺,像睡著了一般摸屠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上粱哼,一...
    開封第一講書人閱讀 51,521評論 1 304
  • 那天季二,我揣著相機與錄音,去河邊找鬼皂吮。 笑死戒傻,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的蜂筹。 我是一名探鬼主播需纳,決...
    沈念sama閱讀 40,288評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼艺挪!你這毒婦竟也來了不翩?” 一聲冷哼從身側(cè)響起兵扬,我...
    開封第一講書人閱讀 39,200評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎口蝠,沒想到半個月后器钟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,644評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡妙蔗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,837評論 3 336
  • 正文 我和宋清朗相戀三年傲霸,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片眉反。...
    茶點故事閱讀 39,953評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡昙啄,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出寸五,到底是詐尸還是另有隱情梳凛,我是刑警寧澤,帶...
    沈念sama閱讀 35,673評論 5 346
  • 正文 年R本政府宣布梳杏,位于F島的核電站韧拒,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏十性。R本人自食惡果不足惜叛溢,卻給世界環(huán)境...
    茶點故事閱讀 41,281評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望烁试。 院中可真熱鬧雇初,春花似錦、人聲如沸减响。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,889評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽支示。三九已至刊橘,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間颂鸿,已是汗流浹背促绵。 一陣腳步聲響...
    開封第一講書人閱讀 33,011評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留嘴纺,地道東北人败晴。 一個月前我還...
    沈念sama閱讀 48,119評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像栽渴,于是被迫代替她去往敵國和親尖坤。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,901評論 2 355

推薦閱讀更多精彩內(nèi)容