探秘Java 7:JVM動態語言支持詳解
原創51CTO推薦專題:Java 7,下一代Java開發平臺詳解
【51CTO精選譯文】JDK 7 增加了對 JSR 292 的支持,在 JVM 中動態類型語言的運行速度將變得更快。這一支持的關鍵在于增加了新的 Java 字節碼,invokedynamic,它用于方法調用,還有新的連接機制,其中包含了一個新的構造:方法句柄(method handle)。此次JDK 7在動態語言支持上的更新是Java平臺發展的重要趨勢之一,在今年6月的JavaOne大會上,這些基于Java平臺的動態語言也十分的活躍。詳情可參考51CTO之前對JavaOne 2009的報導。
動態類型語言和 JVM
JVM 可以執行 Java 程序,將其編譯機器獨立的字節碼。事實上,任何可以使用有效 class 文件表述的功能性語言,都可以運行在 JVM 上。
多年來,運作在 JVM 上語言一直在增加,從 Armed Bear for Common Lisp 到Yoix。動態語言的 JVM 實現也越來越多,比如 JRuby 和 Jython,以及 Groovy 腳本語言。
動態語言的靈活性,尤其是腳本語言,對于實驗性、原型應用程序以及需頻繁更新的程序,都具有獨特的吸引力。這種靈活性源自動態類型。動態類型語言中運行時(runtime)驗證程序中的值是否與預期類型一致,相對的,靜態類型語言,如 Java,是在編譯期間檢查變量類型,而不是值類型。值得一提的是,Java 平臺上另一個前景很被看好的靜態語言就是Scala:包括Java之父和Groovy創始人在內的很多開發者都很看好Scala這個強類型的、可擴展性良好的靜態語言。
通常,動態類型比靜態類型更具靈活性,因為前者允許程序根據運行時的數據生成類型。不過靜態類型語言的執行更為高效,因為它能夠在編譯期間排除錯誤。
動態類型固有的靈活性與 JVM 的執行效率,合二為一。很明顯,這就是它能夠吸引動態編程語言創建者以及使用這些語言構建應用程序的開發者的原因。
JSR 223 動態語言支持的***步
JSR 223: Scripting for the Java Platform 是將動態語言引入 JVM 的***步,它是一個規范,定義了從動態腳本語言代碼訪問 Java 代碼的 API 接口。它還指定了一個 framework 框架,用戶在 Java 應用程序中運行腳本引擎。該規范及其實現使得包含 Java 和腳本代碼的應用程序的創建更為容易。
動態類型語言的問題
為運行在 JVM 上的動態類型語言開發引擎,必須滿足 JVM 所執行的 Java 字節碼的要求,而字節碼專為靜態類型語言設計。對于引擎開發者,當生成字節碼用于方法調用,這種設計一直都是棘手的難點。
方法調用的字節碼要求
靜態類型語言中編譯時進行類型檢查,意味著方法調用,以及它生成的字節碼,需要知道該方法返回的值類型,以及調用中指定的參數類型。
下面為一段 Java 代碼:
- String s = "Hello World";
- System.out.println(s);
這里參數類型是已知的。System.out.println()并不返回值,如果方法返回值,需要指定返回值的類型。
以上代碼相應的字節碼如下:
- ldc #2
- astore_1
- getstatic #3
- aload_1 invokevirtual #4 // Method java/io/PrintStream.println:(I)V
JVM 中字節碼的執行通常包含對操作對象棧(operand stack)中值的操作。操作棧是一個相當于硬件寄存器的虛擬機。通常,字節碼會指示 JVM 局部值壓入操作對象棧,將值從棧中取出放進局部變量中,復制或交換棧中的值,或者執行生成或使用值的操作。
請看 invokevirtual 一行,它調用了一個方法,而不是對操作對象棧進行操作。從該行注釋,我們可以看到,它指出了以下信息:
◆提供方法的接收器(receiver)類:java.io.PrintStream
◆方法名稱:println
◆方法參數類型:(I) 表示 Integer
◆方法返回值: V 表示 void
這些信息相當于方法的簽名。JVM 查找具有該簽名的方法,在這里,就是 java.io.PrintStream 類中的 println:(I)V。如果該方法不在那個類中,JVM 將在類的子類中繼續查找。
滿足要求所進行的拙劣嘗試
為了讓動態類型語言滿足字節碼對方法調用的要求,已經有了多種嘗試,但沒有一種是理想的。
以下面的代碼為例:
- function max (x,y) {
- if x.lessThan(y) then y else x
- }
接收器和參數都沒有指定類型,而對于動態類型語言,直到運行時才提供類型信息,因此,以上代碼未能滿足方法調用需提前獲悉類型的要求,也就不能在 Java 平臺上成功地編譯為字節碼。
問題的解決方法之一是為返回值和方法參數創建虛假的(synthetic)Java 類型。在這里,虛假表示非真實存在。例如,動態類型語言在實現是可能將代碼更改為:
- Interface50 function max (Interface 51 x,Interface52 y) {
- if x.lessThan(y) then y else x
- }
類型 Interface 51 和 Interface52 并不存在,只是為了滿足相應的要求而指定。
另一種方法成為映射調用(relfected invocation),使用 java.lang.reflect.Method 對象調用方法,而避開直接調用方法。這樣也就繞開了指定類型的要求。
第三種方法是為動態語言的實現創建一個獨特的方法調用解釋器(interpreter),以運行在 JVM 上。
虛假類型滿足了 Java 字節碼的要求。但這種方法不但繁復而且會帶來問題。如果動態語言引擎需要更改,相應的實現器(implementer)必須重新創建虛假 Java 類型,這種操作常會出錯。
運行調用也有其自身的局限。例如,java.lang.reflect.Method 對象提供了動態語言所需的方法訪問,但對象必須是運行時可用的特定 Java 類型。雖然,動態語言可以在運行期間提供類型信息,但不是都可以通過用戶映射的規范 Java 類型。
JSR 292 —— 動態語言支持的下一步
JSR 292 為 JVM 引入了一個新的 Java 字節碼指令,invokedynamic,以及一個新的方法連接機制。
方法調用的字節碼指令
Java 虛擬機規范指定了 4 個字節碼,用于方法調用:
◆invokevirtual
◆invokeinterface
◆invokestatic
◆invokespecial
新的 invokedynamic 指令
新的 invokedynamic 字節碼指令的語法與 invokeinterface 指令類似:
- invokedynamic < method-specification> < n>
但,它的 < method-specification> 只需指定方法名稱,對描述符的***要求是它應引用非空對象。
invokeinterface 字節碼指令差不多是這樣的:
- invokedynamic #10;
- //DynamicMethod java/lang/Object.lessThan:(Ljava/lang/Object;)
重要的是,invokedynamic 字節碼指令運行動態語言的實現器(implementer)將方法調用編譯為字節碼,而不必指定目標的類型,該目標包含了方法、調用的返回類型或方法參數類型。這些類型對于執行指令的 JVM 不必是已知的。但如果未提供接收器的類型,JVM 如何找到該方法?畢竟,JVM 需要連接并調用真實類型上的真實方法。答案在于,JSR 292 還包含了一個新的動態類型語言的連接機制。JVM 使用新的連接機制獲取所需的方法。
新的動態連接機制:方法句柄(method handle)
JDK 7 包含了新包,java.dyn,其中包含了與在 Java 平臺中動態語言支持相關的類。其中一個類為 MethodHandle。方法句柄是類型 java.dyn.MethodHandle 的一個簡單對象,該對象包含一個 JVM 方法的匿名引用。
新連接機制還包含一個引導方法(bootstrap 方法),它是一個方法句柄,決定了調用現場(call site)調用的目標方法。調用現場是調用指令的實例,在本節中,它就是 invokedynamic 字節碼指令的實例。包含 invokedynamic 指令的每個類都必須指定引導方法。
JVM ***次遇到具有接收器和參數的 invokedynamic 字節碼時,它調用引導方法。調用語言支持的方法,可以使用術語 up-call 來描述。
引導方法反過來選擇相應的目標方法句柄。然后 JVM 將該方法句柄引用的方法與 invokedynamic 字節碼關聯起來。JVM 下次遇到具有相同接收器和參數的 invokedynamic 字節碼時,它將立即調用之前所選的方法。
方法句柄相當簡單,僅包含一個描述特定類型的類型令牌(type toke)。此外,方法句柄隱式地包含一個與其關聯的 invoke 方法。要調用方法句柄,你需要調用它的 invoke 方法,與調用對象方法類似,即 MethodHandle.invoke(...)。由于每個方法句柄都具有其自身的類型,因此,它只接受那個類型的 invoke 調用。如果調用的類型與方法句柄的類型不匹配,方法句柄將返回異常。
總之,方法句柄提供了一種連接機制,它能夠讓 JVM 根據 invokedynamic 字節碼指令調用正確的方法。但 JVM 遇到 invokedynamic 字節碼時,它將使用方法句柄獲得所需的方法。請注意,相對于映射調用,方法句柄提供了一種更好的方式,來滿足方法調用的字節碼要求。相較而言,方法句柄提供了一種命名和連接方法的方式,而無需考慮方法類型或位置,而且這種方式具有完善的類型安全和本地的執行速度。
通過接口注入(interface injection)在運行時修改類
接口注入能夠在運行時修改類,這樣類就可以構建新的接口。對于動態類型語言,尤其是基本語言,這是一個常見的功能。但它不屬于 JVM 標準的一部分。該功能還處于調研階段,以便加入 JSR 292 中。
在 JVM 中支持接口注入,運行時語言將可以推薦新的功能,以模塊化的方式供其自身使用。例如,假設 JVM 在運行的語言的類或類集合需要串行化的類型,而它尚未在該語言實現。運行時該語言可以定義一個串行定義為可注入的接口。它還可以定義一個注入方法。該方法定義該語言將為哪個類指定新的串行能力。對相關對象調用該注入方法,就可以完成注入。利用接口注入,可以使 JVM 中的動態類型語言很方便地與 JVM 中其他語言進行整合。
總結
多年來,在 JVM 上運行的語言越來越多。在 JVM 中支持動態類型語言,對于使用動態語言的開發者非常具有吸引力。因為,動態類型讓開發者更具靈活性,而且 JVM 具有更好的執行效率。但是,對于動態類型語言,滿足方法調用的字節碼的要求非常困難。為了應對這一難題,JSR 292 提供了新的字節碼 invokedynamic 以及新的基于方法句柄的連接機制。此外,目前還在進行調研在 JSR 292 中引入接口注入,它能夠在運行修改類,從而可以實現新的接口。
原文:New JDK 7 Feature: Support for Dynamically Typed Languages in the Java Virtual Machine
作者:Ed Ort
【編輯推薦】