【轉】解析JDK 7的動態類型語言支持

http://www.infoq.com/cn/articles/jdk-dynamically-typed-languagejava

 

Java虛擬機的字節碼指令集的數量自從Sun公司的第一款Java虛擬機問世至JDK 7來臨以前的十餘年時間裏,一直沒有發生任何變化[1]。隨着JDK 7的發佈,字節碼指令集終於迎來了第一位新成員——invokedynamic指令。這條新增長的指令是JDK 7實現「動態類型語言(Dynamically Typed Language)」支持而進行的改進之一,也是爲JDK 8能夠順利實現Lambda表達式作技術準備。在這篇文章中,咱們將去了解JDK 7這項新特性的出現來龍去脈和它的意義。函數

動態類型語言

在介紹JDK 7提供的動態類型語言支持以前,咱們要先弄明白動態類型語言是什麼?它與Java語言、Java虛擬機有什麼關係?瞭解JDK 7提供動態類型語言支持的技術背景,對理解這個語言特性是很必要的。工具

何謂動態類型語言[2]?動態類型語言的關鍵特徵是它的類型檢查的主體過程是在運行期而不是編譯期進行的,知足這個特徵的語言有不少,經常使用的包括:APL、Clojure、Erlang、Groovy、JavaScript、Jython、Lisp、Lua、PHP、Prolog、Python、Ruby、Smalltalk和Tcl等等。那相對地,在編譯期就進行類型檢查過程的語言,如C++和Java等就是最經常使用的靜態類型語言。性能

 

以爲上面定義過於概念化?那咱們不妨經過兩個例子以最淺顯的方式來講明什麼是「在編譯期/運行期進行」和什麼是「類型檢查」。首先看這段簡單的Java代碼,它是否能正常編譯和運行?優化

public static void main(String[] args) { 
    int[][][] array = new int[1][0][-1]; 
} 

這段代碼可以正常編譯,但運行的時候會報NegativeArraySizeException異常。在《Java虛擬機規範》中明確規定了NegativeArraySizeException是一個運行時異常,通俗一點說,運行時異常就是隻要代碼不運行到這一行就不會有問題。與運行時異常相對應的是鏈接時異常,例如很常見的NoClassDefFoundError便屬於鏈接時異常,即便會致使鏈接時異常的代碼放在一條沒法執行到的分支路徑上,類加載時(Java的鏈接過程不在編譯階段,而在類加載階段)也照樣會拋出異常。 this

不過,在C語言裏,含義相同的代碼的代碼就會在編譯期報錯:編碼

int main(void) {
    int i[1][0][-1]; // GCC拒絕編譯,報「size of array is negative」 
    return 0;
}

由此看來,一門語言的哪種檢查行爲要在運行期進行,哪種檢查要在編譯期進行並無必然的因果邏輯關係,關鍵是在語言規範中人爲規定的,再舉一個例子來解釋「類型檢查」,例以下面這一句再普通不過的代碼:翻譯

obj.println(「hello world」); 

顯然,這行代碼須要一個具體的上下文才有討論的意義,假設它在Java語言中,而且變量obj的類型爲java.io.PrintStream,那obj的值就必須是PrintStream的子類(實現了PrintStream接口的類)纔是合法的。不然,哪怕obj屬於一個確實有用println(String)方法,但與PrintStream接口沒有繼承關係,代碼依然不能運行——由於類型檢查不合法。設計

可是相同的代碼在ECMAScript(JavaScript)中狀況則不同,不管obj具體是何種類型,只要這種類型的定義中確實包含有println(String)方法,那方法調用即可成功。指針

這種差異產生的緣由是Java語言在編譯期間卻已將println(String)方法完整的符號引用(本例中爲一項CONSTANT_InterfaceMethodref_info常量)生成出來,做爲方法調用指令的參數存儲到Class文件中,例以下面這個樣子:

invokevirtual #4; //Method java/io/PrintStream.println:(Ljava/lang/String;)V 

這個符號引用包含了此方法定義在哪一個具體類型之中、方法的名字以及參數順序、參數類型和方法返回值等信息,經過這個符號引用,虛擬機就能夠翻譯出這個方法的直接引用(譬如方法內存地址或者其餘實現形式)。而在ECMAScript等動態類型語言中,變量obj自己是沒有類型的,變量obj的值才具備的類型,編譯時候最多隻能肯定方法名稱、參數、返回值這些信息,而不會去肯定方法所在的具體類型(方法接收者不固定)。「變量無類型而變量值纔有類型」這個特色也是動態類型語言的一個重要特徵。

瞭解了動態和靜態類型語言的區別後,也許讀者的下一個問題就是動態、靜態類型語言二者誰更好,或者誰更加先進?這種比較不會有確切答案,它們都有本身的優勢,選擇哪一種語言是須要權衡的事情。靜態類型語言在編譯期肯定類型,最顯著的好處是編譯器能夠提供嚴謹的類型檢查,這樣與類型相關的問題能在編碼的時候就及時發現,利於穩定性及代碼達到更大的規模。而動態類型語言在運行期肯定類型,這能夠爲開發人員提供更大的靈活性,某些在靜態類型語言中要花大量臃腫代碼來實現的功能,由動態類型語言來實現可能會很清晰簡潔,清晰簡潔一般也就意味着開發效率的提高。

另外,動態、靜態類型語言的界限並不是涇渭分明,如C#是一門靜態類型語言,可是在.NET 4.0中新增長的dynamic關鍵字(它做用就是用來描述一個「無類型」的變量)以及相應的DLR支持後,C# 4.0便擁有了動態語言的特性。

JDK 7與動態類型

如今,咱們回到本專欄的主題,來看看Java語言、虛擬機與動態類型語言之間有什麼關係。Java虛擬機毫無疑問是Java語言的運行平臺,但它的使命並不只限於此,早在1997年出版的《Java虛擬機規範》初版中就規劃了這樣一個願景:「在將來,咱們會對Java虛擬機進行適當的擴展,以便更好的支持其餘語言運行於Java虛擬機之上」。而目前確實已經有許多動態類型語言運行於Java虛擬機之上了,如Clojure、Groovy、Jython和JRuby等等,可以在同一個虛擬機之上能夠實現靜態類型語言的嚴謹與動態類型語言的靈活,這是一件很美妙的事情。

但遺憾的是Java虛擬機層面對動態類型語言的支持一直都有所欠缺,主要表如今方法調用方面:JDK 7之前字節碼指令集中,四條方法調用指令(invokevirtual、invokespecial、invokestatic、invokeinterface)的第一個參數都是被調用的方法的符號引用(CONSTANT_Methodref_info或者CONSTANT_InterfaceMethodref_info常量),前面已經提到過,方法的符號引用在編譯時產生,而動態類型語言只有在運行期才能肯定接收者類型。這樣,在Java虛擬機上實現的動態類型語言就不得不使用「曲線救國」的方式(如編譯時留個佔位符類型,運行時動態生成字節碼實現具體類型到佔位符類型的適配)來實現,這樣勢必讓動態類型語言實現的複雜度增長,也可能帶來額外的性能或者內存開銷。儘管能夠想一些辦法(如Call Site Caching)讓這些開銷儘可能變小,但這種底層問題終歸是應當在虛擬機層次上去解決才最合適,所以在Java虛擬機層面上提供動態類型的直接支持就成爲了Java平臺的發展趨勢之一,這就是JDK 7(JSR-292)中invokedynamic指令以及java.lang.invoke包出現的技術背景。

java.lang.invoke包

JDK 7實現了JSR 292 《Supporting Dynamically Typed Languages on the Java Platform》,新加入的java.lang.invoke包[3]是就是JSR 292的一個重要組成部分,這個包的主要目的是在以前單純依靠符號引用來肯定調用的目標方法這條路以外,提供一種新的動態肯定目標方法的機制,稱爲Method Handle。這個表達也很差懂?那不妨把Method Handle與C/C++中的Function Pointer,或者C#裏面的Delegate類比一下。舉個例子,若是咱們要實現一個帶謂詞的排序函數,在C/C++中經常使用作法是把謂詞定義爲函數,用函數指針來把謂詞傳遞到排序方法,像這樣:

void sort(int list[], const int size, int (*compare)(int, int)) 

但Java語言中作不到這一點,沒有辦法單獨把一個函數做爲參數進行傳遞。廣泛的作法是設計一個帶有compare()方法的Comparator接口,以實現了這個接口的對象做爲參數,例如Collections.sort()就是這樣定義的:

void sort(List list, Comparator c)

不過,在擁有Method Handle以後,Java語言也能夠擁有相似於函數指針或者委託的方法別名的工具了。下面代碼演示了MethodHandle的基本用途,不管obj是何種類型(臨時定義的ClassA抑或是實現PrintStream接口的實現類System.out),均可以正確調用到println()方法。

import static java.lang.invoke.MethodHandles.lookup;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
/** 
 * JSR 292 MethodHandle基礎用法演示
 * @author IcyFenix
 */
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最終是哪一個實現類,下面這句都能正確調用到println方法。 
        getPrintlnMH(obj).invokeExact("icyfenix");
    }
    private static MethodHandle getPrintlnMH(Object reveiver) throws Throwable {
        // MethodType:表明「方法類型」,包含了方法的返回值(methodType()的第一個參數)和具體參數(methodType()第二個及之後的參數)。 
        MethodType mt = MethodType.methodType(void.class, String.class);
        // lookup()方法來自於MethodHandles.lookup,這句的做用是在指定類中查找符合給定的方法名稱、方法類型,而且符合調用權限的方法句柄。 
        // 由於這裏調用的是一個虛方法,按照Java語言的規則,方法第一個參數是隱式的,表明該方法的接收者,也便是this指向的對象,這個參數之前是放在參數列表中進行傳遞,如今提供了bindTo()方法來完成這件事情。 
        return lookup().findVirtual(reveiver.getClass(), "println", mt).bindTo(reveiver);
    }
}

方法getPrintlnMH()中其實是模擬了invokevirtual指令的執行過程,只不過它的分派邏輯並不是固化在Class文件的字節碼上的,而是經過一個具體方法來實現。而這個方法自己的返回值(MethodHandle對象),能夠視爲對最終調用方法的一個「引用」。以此爲基礎,有了MethodHandle就能夠寫出相似於這樣的函數聲明瞭:

void sort(List list, MethodHandle compare) 

從上面的例子看來,使用MethodHandle並無多少困難,不過看完它的用法以後,讀者大概就會疑問到,相同的事情,用反射不是早就能夠實現了嗎?

確實,僅站在Java語言的角度看,MethodHandle的使用方法和效果上與Reflection都有衆多類似之處。不過,它們也有如下這些區別:

  • Reflection和MethodHandle機制本質上都是在模擬方法調用,可是Reflection是在模擬Java代碼層次的方法調用,而MethodHandle是在模擬字節碼層次的方法調用。在MethodHandles.Lookup上的三個方法findStatic()、findVirtual()、findSpecial()正是爲了對應於invokestatic、invokevirtual & invokeinterface和invokespecial這幾條字節碼指令的執行權限校驗行爲,而這些底層細節在使用Reflection API時是不須要關心的。
  • Reflection中的java.lang.reflect.Method對象遠比MethodHandle機制中的java.lang.invoke.MethodHandle對象所包含的信息來得多。前者是方法在Java一端的全面映像,包含了方法的簽名、描述符以及方法屬性表中各類屬性的Java端表示方式,還包含有執行權限等的運行期信息。然後者僅僅包含着與執行該方法相關的信息。用開發人員通俗的話來說,Reflection是重量級,而MethodHandle是輕量級。
  • 因爲MethodHandle是對字節碼的方法指令調用的模擬,那理論上虛擬機在這方面作的各類優化(如方法內聯),在MethodHandle上也應當能夠採用相似思路去支持(但目前實現還不完善)。而經過反射去調用方法則不行。

MethodHandle與Reflection除了上面列舉的區別外,最關鍵的一點還在於去掉前面討論施加的前提「僅站在Java語言的角度看」以後:Reflection API的設計目標是隻爲Java語言服務的,而MethodHandle則設計爲可服務於全部Java虛擬機之上的語言,其中也包括了Java語言而已。

invokedynamic指令

本文一開始就提到了JDK 7爲了更好地支持動態類型語言,引入了第五條方法調用的字節碼指令invokedynamic,但前面一直沒有再提到它,甚至把以前使用MethodHandle的示例代碼反編譯後也不會看見invokedynamic的身影,它到底有什麼應用呢?

某種程度上能夠說invokedynamic指令與MethodHandle機制的做用是同樣的,都是爲了解決原有四條invoke*指令方法分派規則固化在虛擬機之中的問題,把如何查找目標方法的決定權從虛擬機轉嫁到具體用戶代碼之中,讓用戶(包含其餘語言的設計者)有更高的自由度。並且,它們二者的思路也是可類比的,能夠想象做爲了達成同一個目的,一個用上層代碼和API來實現,另外一個是用字節碼和Class中其餘屬性和常量來完成。所以,若是前面MethodHandle的例子看懂了,理解invokedynamic指令並不困難。

每一處含有invokedynamic指令的位置都被稱做「動態調用點(Dynamic Call Site)」,這條指令的第一個參數再也不是表明方法符號引用的CONSTANT_Methodref_info常量,而是變爲JDK 7新加入的CONSTANT_InvokeDynamic_info常量,從這個新常量中能夠獲得3項信息:引導方法(Bootstrap Method,此方法存放在新增的BootstrapMethods屬性中)、方法類型(MethodType)和名稱。引導方法是有固定的參數,而且返回值是java.lang.invoke.CallSite對象,這個表明真正要執行的目標方法調用。根據CONSTANT_InvokeDynamic_info常量中提供的信息,虛擬機能夠找到而且執行引導方法,從而得到一個CallSite對象,最終調用要執行的目標方法上。咱們仍是照例拿一個實際例子來解釋這個過程吧。以下面代碼清單所示:

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();
    }
}

這段代碼與前面MethodHandleTest的做用基本上是同樣的,雖然筆者沒有加以註釋,可是閱讀起來應當不困難。真沒讀懂也沒關係,我沒寫註釋的緣由是這段代碼並不是寫給人看的(@_@,我不是在罵人)。因爲目前光靠Java語言的編譯器javac沒有辦法生成帶有invokedynamic 指令的字節碼(曾經有一個java.dyn.InvokeDynamic的語法糖能夠實現,但後來被取消了),因此只能用一些變通的辦法,John Rose(Da Vinci Machine Project的Leader)編寫了一個把程序的字節碼轉換爲使用invokedynamic的簡單工具INDY[4]來完成這件事情,咱們要使用這個工具來產生最終要的字節碼,所以這個示例代碼中的方法名稱不能亂改,更不能把幾個方法合併到一塊兒寫。

把上面代碼編譯、轉換後從新生成的字節碼以下(結果使用javap輸出,因版面緣由,精簡了許多無關的內容):

Constant pool: 
    #121 = NameAndType #33:#30 // testMethod:(Ljava/lang/String;)V 
    #123 = InvokeDynamic #0:#121 // #0:testMethod:(Ljava/lang/String;)V 
    public static void main(java.lang.String[]) throws java.lang.Throwable; 
Code: 
    stack=2, locals=1, args_size=1 
0: ldc #23 // String abc 
2: invokedynamic #123, 0 // InvokeDynamic #0:testMethod:(Ljava/lang/String;)V 
7: nop 
8: return 
    public static java.lang.invoke.CallSite BootstrapMethod(java.lang.invoke.MethodHandles$Lookup, java.lang.String, java.lang.invoke.MethodType) throws java.lang.Throwable; 
Code: 
    stack=6, locals=3, args_size=3 
0: new #63 // class java/lang/invoke/ConstantCallSite 
3: dup 
4: aload_0 
5: ldc #1 // class org/fenixsoft/InvokeDynamicTest 
7: aload_1 
8: aload_2 
9: invokevirtual #65 // Method         java/lang/invoke/MethodHandles$Lookup.findStatic:(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle; 
12: invokespecial #71 // Method java/lang/invoke/ConstantCallSite."<init>":(Ljava/lang/invoke/MethodHandle;)V 
15: areturn 

從main()方法的字節碼中可見,本來的方法調用指令已經被替換爲invokedynamic了,它的參數爲第123項常量(第二個值爲0的參數在HotSpot中用不到,與invokeinterface那個的值爲0的參數同樣是佔位的):

2: invokedynamic #123, 0 // InvokeDynamic #0:testMethod:(Ljava/lang/String;)V 

從常量池中可見,第123項常量顯示「#123 = InvokeDynamic #0:#121」說明它是一項CONSTANT_InvokeDynamic_info類型常量,常量值中前面「#0」表明引導方法取BootstrapMethods屬性表的第0項(javap沒有列出屬性表的具體內容,不過示例中僅有一個引導方法,即BootstrapMethod()),然後面的「#121」表明引用第121項類型爲CONSTANT_NameAndType_info的常量,從個常量中能夠獲取方法名稱和描述符,既後面輸出的「testMethod:(Ljava/lang/String;)V」。

再看BootstrapMethod(),這個方法Java源碼中沒有,是INDY產生的,可是它的字節碼很容易讀懂,全部邏輯就是調用MethodHandles$Lookup的findStatic()方法,產生testMethod()方法的MethodHandle,而後用它建立一個ConstantCallSite對象。最後,這個對象返回給invokedynamic指令實現對testMethod()方法的調用,invokedynamic指令的調用過程到此就宣告完成了。

相關文章
相關標籤/搜索