讀古今文學網 > Java程序員修煉之道 > 5.5 Invokedynamic >

5.5 Invokedynamic

本節主要針對Java 7中最複雜的新特性之一。儘管這個特性十分強大,但它並不是給所有開發人員準備的,它只會出現在非常高級的用例中。目前來看,這個特性是為框架開發人員和非Java語言準備的。

也就是說如果你對平台底層如何運轉不感興趣,對新的字節碼細節毫不關心,請跳到小結部分或直接進入下一章,沒關係的。

如果你還在,很好。接下來我們可以向你介紹invokedynamic的出現是多麼不同尋常。Java 7引入了一個嶄新的字節碼,這在Java世界中可是從來沒有過的大事件。這個字節碼新秀就是invokedynamic,一種新的調用指令,是用來做方法調用的。它可以用來告訴VM必須延遲確定要調用哪個方法。也就是說VM不用像往常一樣在編譯或連接時就敲定所有細節。

相反,需要什麼方法在運行時決定。通過調用一個輔助方法來確定應該調用哪個方法。

javac不會產生invokedynamic

在Java 7中,Java語言還不能直接支持invokedynamic,沒有哪個Java表達式會被javac直接編譯成invokedynamic。人們希望Java 8會增加更多的語言結構(比如默認方法)來使用這些動態能力。

invokedynamic是為非java語言準備的。添加它是為了讓動態語言能夠利用Java 7 VM,不過有些聰明的Java框架也找到了讓invokedynamic為它們服務的辦法。

我們在本節中會給出invokedynamic的工作細節,還會給出一個詳細的例子——反編譯一個利用新字節碼的調用點。注意,要使用那些用到invokedynamic的語言和框架不一定要完全搞清楚這些內容。

5.5.1 invokedynamic如何工作

為了支持invokedynamic,Java 7又新增加了幾條常量池定義。這些是在Java 6技術中無法提供的支持。

invokedynamic指令的索引必須指向類型為CONSTANT_InvokeDynamic的常量。這個常量上是兩個16位的索引(也就是4字節)。第一個索引指向方法表(用來確定要調用什麼)。它們被稱為引導方法(有時簡寫為BSM),並且必須是靜態的,還要有確定的參數簽名。第二個索引指向CONSTANT_NameAndType

從中可以看出CONSTANT_InvokeDynamic和普通的CONSTANT_MethodRef差不多,只是CONSTANT_MethodRef指明在哪個類的常量池裡找尋方法,而invokedynamic調用則通過引導方法來尋找答案。

引導方法會返回一個CallSite實例,用它來接收與調用點相關的信息,並連接動態調用。調用點中有一個MethodHandle,調用點在這裡起一個代理的作用,對它的所有調用實際上就是對MethodHandle的調用。1

1 詳情請參見CallSite的Javadoc:http://cr.openjdk.java.net/~jrose/pres/indy-javadoc-mlvm/java/lang/invoke/CallSite.html。——譯者注

invokedynamic一開始並沒有目標方法(還沒連接)。在第一次調用時,該點的引導方法被調用。引導方法返回一個CallSite,它被連接到invokedynamic指令上。該過程如圖5-5所示。

圖5-5 虛擬vs動態調用

連接上CallSite後,就可以調用真正的方法了,即CallSite持有的MethodHandle所指向的方法。這種設定表明JIT編譯器可以像優化invokevirtual調用那樣優化invokedynamic調用。下一章會討論更多有關優化的內容。

還有一點值得注意,某些CallSite對象是可以重連的(在它們的生命期內指向不同的目標方法)。一些動態語言會大量使用這一特性。

下一節會給出一個簡單的例子,我們可以看到invokedynamic調用在字節碼中如何表示。

5.5.2 示例:反編譯invokedynamic調用

如前所述,Java 7中沒有支持invokedynamic的Java語法。要得到帶有動態調用指令的.class文件,你只能向字節碼處理類庫求助。ASM類庫(http://asm.ow2.org/)就是一個不錯的選擇——它是一個工業級類庫,在Java框架中得到了廣泛應用。

我們可以用這個類庫構造一個包含invokedynamic指令的類,然後將其轉換為字節流。這既可以寫到磁盤裡,也可以交給類加載器插入到運行的VM中。

一個簡單的例子是讓ASM產生的類包含一種invokedynamic指令的靜態方法。這個方法可以由普通的Java代碼調用——它封裝(或隱藏)了真正調用的動態本質。作為 invokedynamic開發工作的一部分,Remi Forax和ASM團隊提供了一個簡單的工具來產生這樣的測試類。ASM是第一批完全支持新字節碼的工具之一。

讓我們來看一下這種封裝方法的字節碼:

public static java.math.BigDecimal invokedynamic;
  Code:
     0: invokedynamic #22, 0 // InvokeDynamic #0:_:Ljava/math/BigDecimal;
     5: areturn
  

到目前為止還沒什麼看頭,因為複雜性主要體現在常量池中。我們來看看和動態調用相關的常量池條目:

BootstrapMethods:
  0: #17 invokestatic test/invdyn/DynamicIndyMakerMain.bsm:
  ➥ (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;
  ➥ Ljava/lang/invoke/MethodType;Ljava/lang/Object;)
  ➥ Ljava/lang/invoke/CallSite;
  Method arguments:
  #19 1234567890.1234567890
  #10 = Utf8            Ljava/math/BigDecimal;
  #18 = Utf8           1234567890.1234567890
  #19 = String         #18   //  1234567890.1234567890
  #20 = Utf8            _
  #21 = NameAndType    #20:#10 // _:Ljava/math/BigDecimal;
  #22 = InvokeDynamic  #0:#21  // #0:_:Ljava/math/BigDecimal;
  

要想完全搞清楚確實得花點心思琢磨琢磨。我們逐一來看一下。

  • invokedynamic操作碼在條目#22中。它指向引導方法#0和NameAndType#21。
  • 在#0的BSM是類DynamicIndyMakerMain中的普通靜態方法bsm。它有BSM的正確簽名。
  • 條目#21給出了這個動態連接點的名稱「_」,還有返回類型BigDecimal(保存在#10)。
  • 條目#19是傳入引導方法的靜態參數。

如你所見,這裡需要做很多基礎工作來保證類型安全。但在運行時出錯的方式仍然還有很多,但這種機製作了很大貢獻,它在保留了靈活性的同時提供了安全性。

注意 BootstrapMethods方法指向方法句柄而不是直接指向方法,這提供了額外的間接性,或者說靈活性。在前面的討論中我們並沒有涉及,因為它可能會混淆正在發生的事情,對於理解這種機制如何工作並沒有實質性的幫助。

到此為止,我們已經結束了對invokedynamic和字節碼及類加載內部工作機制的討論。