讀古今文學網 > Java 8實戰 > 附錄 D Lambda表達式和JVM字節碼 >

附錄 D Lambda表達式和JVM字節碼

你可能會好奇Java編譯器是如何實現Lambda表達式,而Java虛擬機又是如何對它們進行處理的。如果你認為Lambda表達式就是簡單地被轉換為匿名類,那就太天真了,請繼續閱讀下去。本附錄通過審視編譯生成的.class文件,簡要地討論Java是如何編譯Lambda表達式的。

D.1 匿名類

我們在第2章已經介紹過,匿名類可以同時聲明和實例化一個類。因此,它們和Lambda表達式一樣,也能用於提供函數式接口的實現。

由於Lambda表達式提供了函數式接口中抽像方法的實現,這讓人有一種感覺,似乎在編譯過程中讓Java編譯器直接將Lambda表達式轉換為匿名類更直觀。不過,匿名類有著種種不盡如人意的特性,會對應用程序的性能帶來負面影響。

  • 編譯器會為每個匿名類生成一個新的.class文件。這些新生成的類文件的文件名通常以ClassName$1這種形式呈現,其中ClassName是匿名類出現的類的名字,緊跟著一個美元符號和一個數字。生成大量的類文件是不利的,因為每個類文件在使用之前都需要加載和驗證,這會直接影響應用的啟動性能。如果將Lambda表達式轉換為匿名類,每個Lambda表達式都會產生一個新的類文件,這是我們不期望發生的。

  • 每個新的匿名類都會為類或者接口產生一個新的子類型。如果你為了實現一個比較器,使用了一百多個不同的Lambda表達式,這意味著該比較器會有一百多個不同的子類型。這種情況下,JVM的運行時性能調優會變得更加困難。

D.2 生成字節碼

Java的源代碼文件會經由Java編譯器編譯為Java字節碼。之後JVM可以執行這些生成的字節碼運行應用。編譯時,匿名類和Lambda表達式使用了不同的字節碼指令。你可以通過下面這條命令查看任何類文件的字節碼和常量池:

javap -c -v ClassName

  

我們試著使用Java 7中舊的格式實現了Function接口的一個實例,代碼如下所示。

代碼清單D-1 以匿名內部類的方式實現的一個Function接口

import java.util.function.Function;
public class InnerClass {
    Function<Object, String> f = new Function<Object, String> {
        @Override
        public String apply(Object obj) {
            return obj.toString;
        }
    };
}

  

這種方式下,和Function對應,以匿名內部類形式生成的字節碼看起來就像下面這樣:

 0: aload_0
 1: invokespecial #1       // Method java/lang/Object."<init>":V
 4: aload_0
 5: new           #2       // class InnerClass$1
 8: dup
 9: aload_0
10: invokespecial #3       // Method InnerClass$1."<init>":(LInnerClass;)V
13: putfield      #4       // Field f:Ljava/util/function/Function;
16: return

  

這段代碼展示了下面這些編譯中的細節。

  • 通過字節碼操作new,一個InnerClass$1類型的對象被實例化了。與此同時,一個指向新創建對象的引用會被壓入棧。

  • dup操作會複製棧上的引用。

  • 接著,這個值會被invokespecial指令處理,該指令會初始化對象。

  • 棧頂現在包含了指向對象的引用,該值通過putfield指令保存到了LambdaBytecode類的f1字段。

InnerClass$1是由編譯器為匿名類生成的名字。如果你想要再次確認這一情況,也可以查看InnerClass$1類文件,你可以看到Function接口的實現代碼如下:

class InnerClass$1 implements
          java.util.function.Function<java.lang.Object, java.lang.String> {
  final InnerClass this$0;
  public java.lang.String apply(java.lang.Object);
    Code:
       0: aload_1
       1: invokevirtual #3 //Method
                             java/lang/Object.toString:Ljava/lang/String;
       4: areturn
}

  

D.3 用InvokeDynamic力挽狂瀾

現在,我們試著採用Java 8中新提供的Lambda表達式來完成同樣的功能。我們會查看下面這段代碼清單生成的類文件。

代碼清單D-2 使用Lambda表達式實現的Function

import java.util.function.Function;
public class Lambda {
    Function<Object, String> f = obj -> obj.toString;
}

  

你會看到下面這些字節碼指令:

 0: aload_0
 1: invokespecial #1    // Method java/lang/Object."<init>":V
 4: aload_0
 5: invokedynamic #2, 0 // InvokeDynamic
                           #0:apply:Ljava/util/function/Function;
10: putfield      #3    // Field f:Ljava/util/function/Function;
13: return

  

我們已經解釋過將Lambda表達式轉換為內部匿名類的缺點,通過這段字節碼你可以再次確認二者之間巨大的差別。創建額外的類現在被invokedynamic指令替代了。

invokedynamic指令

字節碼指令invokedynamic最初被JDK7引入,用於支持運行於JVM上的動態類型語言。執行方法調用時,invokedynamic添加了更高層的抽像,使得一部分邏輯可以依據動態語言的特徵來決定調用目標。這一指令的典型使用場景如下:

def add(a, b) { a + b }

  

這裡ab的類型在編譯時都未知,有可能隨著運行時發生變化。由於這個原因,JVM首次執行invokedynamic調用時,它會查詢一個bootstrap方法,該方法實現了依賴語言的邏輯,可以決定選擇哪一個方法進行調用。bootstrap方法返回一個鏈接調用點(linked call site)。很多情況下,如果add方法使用兩個int類型的變量,緊接下來的調用也會使用兩個int類型的值。所以,每次調用也沒有必要都重新選擇調用的方法。調用點自身就包含了一定的邏輯,可以判斷在什麼情況下需要進行重新鏈接。

代碼清單D-2中,使用invokedynamic指令的目的略微有別於我們最初介紹的那一種。這個例子中,它被用於延遲Lambda表達式到字節碼的轉換,最終這一操作被推遲到了運行時。換句話說,以這種方式使用invokedynamic,可以將實現Lambda表達式的這部分代碼的字節碼生成推遲到運行時。這種設計選擇帶來了一系列好結果。

  • Lambda表達式的代碼塊到字節碼的轉換由高層的策略變成了純粹的實現細節。它現在可以動態地改變,或者在未來版本中得到優化、修改,並且保持了字節碼的後向兼容性。

  • 沒有帶來額外的開銷,沒有額外的字段,也不需要進行靜態初始化,而這些如果不使用Lambda,就不會實現。

  • 對無狀態非捕獲型Lambda,我們可以創建一個Lambda對象的實例,對其進行緩存,之後對同一對象的訪問都返回同樣的內容。這是一種常見的用例,也是人們在Java 8之前就慣用的方式;比如,以static final變量的方式聲明某個比較器實例。

  • 沒有額外的性能開銷,因為這些轉換都是必須的,並且結果也進行了鏈接,僅在Lambda首次被調用時需要轉換。其後所有的調用都能直接跳過這一步,直接調用之前鏈接的實現。

D.4 代碼生成策略

將Lambda表達式的代碼體填入到運行時動態創建的靜態方法,就完成了Lambda表達式的字節碼轉換。無狀態Lambda在它涵蓋的範圍內不保持任何狀態信息,就像我們在代碼清單D-2中定義的那樣,字節碼轉換時它是所有Lambda中最簡單的一種類型。這種情況下,編譯器可以生成一個方法,該方法含有該Lambda表達式同樣的簽名,所以最終轉換的結果從邏輯上看起來就像下面這樣:

public class Lambda {
    Function<Object, String> f = [dynamic invocation of lambda$1]

    static String lambda$1(Object obj) {
        return obj.toString;
    }
}

  

Lambda表達式中包含了final(或者效果上等同於final)的本地變量或者字段的情況會稍微複雜一些,就像下面的這個例子:

public class Lambda {
    String header = "This is a ";
    Function<Object, String> f = obj -> header + obj.toString;
}

  

這個例子中,生成方法的簽名不會和Lambda表達式一樣,因為它還需要攜帶參數來傳遞上下文中額外的狀態。為了實現這一目標,最簡單的方案是在Lambda表達式中為每一個需要額外保存的變量預留參數,所以實現前面Lambda表達式的生成方法會像下面這樣:

public class Lambda {
    String header = "This is a ";
    Function<Object, String> f = [dynamic invocation of lambda$1]

    static String lambda$1(String header, Object obj) {
        return obj -> header + obj.toString;
    }
}

  

更多關於Lambda表達式轉換流程的內容,可以訪問如下地址:http://cr.openjdk.java.net/~briangoetz/lambda/lambda-translation.html。