java8 探討與分析匿名內部類、lambda表達式、方法引用的底層實現

問題解決思路:查看編譯生成的字節碼文件html

同步發佈地址:https://www.cnblogs.com/chenj...java

思路一:express

  1. 編譯 javac fileName.java
  2. 反編譯 javap -v -p fileName.class ; 這一步能夠看到字節碼。

思路二:
運行階段保留jvm生成的類
java -Djdk.internal.lambda.dumpProxyClasses fileName.class數組

不錯的博客:https://blog.csdn.net/zxhoo/a...緩存


本人旨在探討匿名內部類、lambda表達式(lambda expression),方法引用(method references )的底層實現,包括實現的階段(第一次編譯期仍是第二次編譯)和實現的原理。markdown

測試匿名內部類的實現

建議去對照着完整的代碼來看 源碼連接
基於strategy類,使用匿名內部類,main函數的代碼以下,稱做test1app

Strategy strategy = new Strategy() {
        @Override
        public String approach(String msg) {
            return "strategy changed : "+msg.toUpperCase() + "!";
        }
    };
    Strategize s = new Strategize("Hello there");
    s.communicate();
    s.changeStrategy(strategy);
    s.communicate();

第一步:如今對其使用javac編譯,在Strategize.java的目錄裏,命令行運行javac Strategize.java,結果咱們能夠看到生成了5個.class文件,咱們預先定義的只有4個class,而如今卻多出了一個,說明編譯期幫咱們生成了一個class,其內容以下:jvm

class Strategize$1 implements Strategy {
    Strategize$1() {
    }

    public String approach(String var1) {
        return var1.toUpperCase();
    }
}

第二部:對生成的 Strategize.class 進行反編譯,運行javap -v -c Strategize.class,在輸出的結尾能夠看到下面信息:ide

NestMembers:
  com/langdon/java/onjava8/functional/Strategize$1
InnerClasses:
  #9;                                     // class com/langdon/java/onjava8/functional/Strategize$1

說明,這個Strategize$1的確是Strategize的內部類。
這個類是命名是有規範的,做爲Strategize的第一個內部類,因此命名爲Strategize$1。若是咱們在測試的時候多寫一個匿名內部類,結果會怎樣?
咱們修改main()方法,多寫一個匿名內部類,稱作test2函數

Strategy strategy1 = new Strategy() {
        @Override
        public String approach(String msg) {
            return "strategy1  : "+msg.toUpperCase() + "!";
        }
    };
    Strategy strategy2 = new Strategy() {
        @Override
        public String approach(String msg) {
            return "strategy2  : "+msg.toUpperCase() + "!";
        }
    };
    Strategize s = new Strategize("Hello there");
    s.communicate();
    s.changeStrategy(strategy1);
    s.communicate();
    s.changeStrategy(strategy2);
    s.communicate();

繼續使用javac編譯一下;結果與預想的意義,多生成了2個類,分別是Strategize$1Strategize$2,二者是實現方式是相同的,都是實現了Strategy接口的class

小結

到此,能夠說明匿名內部類的實現:第一次編譯的時候經過字節碼工具多生成一個class來實現的。

測試lambda表達式

第一步:修改test2的代碼,把strategy1改用lambda表達式實現,稱做test3

Strategy strategy1 = msg -> "strategy1  : "+msg.toUpperCase() + "!";
    Strategy strategy2 = new Strategy() {
        @Override
        public String approach(String msg) {
            return "strategy2  : "+msg.toUpperCase() + "!";
        }
    };
    Strategize s = new Strategize("Hello there");
    s.communicate();
    s.changeStrategy(strategy1);
    s.communicate();
    s.changeStrategy(strategy2);
    s.communicate();

第二步:繼續使用javac編譯,結果只多出了一個class,名爲Strategize$1,這是用匿名內部類產生的,可是lambda表達式的實現還看不到。但此時發現main()函數的代碼在NetBeans中已經沒法反編譯出來,是NetBeans的反編譯器不夠強大?嘗試使用在線反編譯器,結果的部分以下

public static void main(String[] param0) {
      // $FF: Couldn't be decompiled
   }

   // $FF: synthetic method
   private static String lambda$main$0(String var0) {
      return var0.toUpperCase();
   }

第三步:使用javap反編譯,能夠看到在main()方法的後面多出了一個函數,以下描述

private static java.lang.String lambda$main$0(java.lang.String);
    descriptor: (Ljava/lang/String;)Ljava/lang/String;
    flags: (0x100a) ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #17                 // Method java/lang/String.toUpperCase:()Ljava/lang/String;
         4: invokedynamic #18,  0             // InvokeDynamic #1:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
         9: areturn
      LineNumberTable:
        line 48: 0

到此,咱們只能見到,在第一次編譯後僅僅是編譯期多生成了一個函數,並無爲lambda表達式多生成一個class。
關於這個方法lambda$main$0的命名:以lambda開頭,由於是在main()函數裏使用了lambda表達式,因此帶有$main表示,由於是第一個,因此$0。

第四步:運行Strategize,回到src目錄,使用java 完整報名.Strategize,好比我使用的是java com.langdon.java.onjava8.functional.test3.Strategize,結果是直接運行的mian函數,類文件並無發生任何變化。

第五步:加jvm啓動屬性,若是咱們在啓動JVM的時候設置系統屬性"jdk.internal.lambda.dumpProxyClasses"的話,那麼在啓動的時候生成的class會保存下來。使用java命令以下

java -Djdk.internal.lambda.dumpProxyClasses com.langdon.java.onjava8.functional.test3.Strategize

此時,我看到了一個新的類,以下:

import java.lang.invoke.LambdaForm.Hidden;

// $FF: synthetic class
final class Strategize$$Lambda$1 implements Strategy {
    private Strategize$$Lambda$1() {
    }

    @Hidden
    public String approach(String var1) {
        return Strategize.lambda$main$0(var1);
    }
}

synthetic class說明這個類是經過字節碼工具自動生成的,注意到,這個類是final,實現了Strategy接口,接口是實現很簡單,就是調用了第一次編譯時候生產的Strategize.lambda$main$0()方法。從命名上能夠看出這個類是實現lambda表達式的類和以及Strategize的內部類。

小結

lambda表達式與普通的匿名內部類的實現方式不同,在第一次編譯階段只是多增了一個lambda方法,並經過invoke dynamic 指令指明瞭在第二次編譯(運行)的時候須要執行的額外操做——第二次編譯時經過java/lang/invoke/LambdaMetafactory.metafactory 這個工廠方法來生成一個class(其中參數傳入的方法就是第一次編譯時生成的lambda方法。)
這個操做最終仍是會生成一個實現lambda表達式的內部類。

測試方法引用

爲了測試方法引用(method reference),對上面的例子作了一些修改,具體看test4.

第一步:運行javac Strategize.java,並無生產額外的.class文件,都是預約義的。這點與lambda表達式是一致的。但NetBeans對Strategize.class的mian()方法反編譯失敗,嘗試使用上文提到的反編譯器,結果也是同樣。

第二步:嘗試使用javap -v -p 反編譯Strategize.class,發現與lambda表達式類似的地方

InnerClasses:
  public static final #82= #81 of #87;    // Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
  0: #45 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #46 (Ljava/lang/String;)Ljava/lang/String;
      #47 REF_invokeStatic com/langdon/java/onjava8/functional/test4/Unrelated.twice:(Ljava/lang/String;)Ljava/lang/String;
      #46 (Ljava/lang/String;)Ljava/lang/String;
  1: #45 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #46 (Ljava/lang/String;)Ljava/lang/String;
      #52 REF_invokeVirtual com/langdon/java/onjava8/functional/test4/Unrelated.third:(Ljava/lang/String;)Ljava/lang/String;
      #46 (Ljava/lang/String;)Ljava/lang/String;

從這裏能夠看出,方法引用的實現方式與lambda表達式是很是類似的,都是在第二次編譯(運行)的時候調用java/lang/invoke/LambdaMetafactory.metafactory 這個工廠方法來生成一個class,其中方法引用不須要在第一次編譯時生成額外的lambda方法。

第三步:使用jdk.internal.lambda.dumpProxyClasses參數運行。以下

java -Djdk.internal.lambda.dumpProxyClasses com.langdon.java.onjava8.functional.test4.Strategize

結果 jvm 額外生成了2個.class文件,分別是 Strategize\\$\\$Lambda$1Strategize$$Lambda$2, (這裏的雙斜槓是不存在的,因爲markdown語法排版而添加)。從這點能夠看出方法引用在第二次編譯時的實現方式與lambda表達式是同樣的,都是藉助字節碼工具生成相應的class。兩個類的代碼以下 (由NetBeans反編譯獲得)

//for Strategize$$Lambda$1
package com.langdon.java.onjava8.functional.test4;

import java.lang.invoke.LambdaForm.Hidden;

// $FF: synthetic class
final class Strategize$$Lambda$1 implements Strategy {
    private Strategize$$Lambda$1() {
    }

    @Hidden
    public String approach(String var1) {
        return Unrelated.twice(var1);
    }
}

// for Strategize$$Lambda$2
package com.langdon.java.onjava8.functional.test4;

import java.lang.invoke.LambdaForm.Hidden;

// $FF: synthetic class
final class Strategize$$Lambda$2 implements StrategyDev {
    private final Unrelated arg$1;

    private Strategize$$Lambda$2(Unrelated var1) {
        this.arg$1 = var1;
    }

    private static StrategyDev get$Lambda(Unrelated var0) {
        return new Strategize$$Lambda$2(var0);
    }

    @Hidden
    public String approach(String var1) {
        return this.arg$1.third(var1);
    }
}

小結

方法引用在第一次編譯的時候並無生產額外的class,也沒有像lambda表達式那樣生成一個static方法,而只是使用invoke dynamic標記了(這點與lambda表達式同樣),在第二次編譯(運行)時會調用java/lang/invoke/LambdaMetafactory.metafactory 這個工廠方法來生成一個class,其中參數傳入的方法就是方法引用的實際方法。這個操做與lambda表達式同樣都會生成一個匿名內部類。

三種實現方式的總結

方式 javac編譯 javap反編譯 jvm調參並第二次編譯 (運行)
匿名內部類 額外生成class 未見invoke dynamic指令 無變化
lambda表達式 未生成class,但額外生成了一個static的方法 發現invoke dynamic 發現額外的class
方法引用 未額外生成 發現invoke dynamic 發現額外的class

對於lambda表達式,爲何java8要這樣作?

下面的譯本,原文Java-8-Lambdas-A-Peek-Under-the-Hood

匿名內部類具備可能影響應用程序性能的不受歡迎的特性。

  1. 編譯器爲每一個匿名內部類生成一個新的類文件。生成許多類文件是不可取的,由於每一個類文件在使用以前都須要加載和驗證,這會影響應用程序的啓動性能。加載多是一個昂貴的操做,包括磁盤I/O和解壓縮JAR文件自己。
  2. 若是lambdas被轉換爲匿名內部類,那麼每一個lambda都有一個新的類文件。因爲每一個匿名內部類都將被加載,它將佔用JVM的元空間(這是Java 8對永久生成的替代)。若是JVM將每一個此類匿名內部類中的代碼編譯爲機器碼,那麼它將存儲在代碼緩存中。此外,這些匿名內部類將被實例化爲單獨的對象。所以,匿名內部類會增長應用程序的內存消耗。爲了減小全部這些內存開銷,引入一種緩存機制多是有幫助的,這將促使引入某種抽象層。
  3. 最重要的是,從第一天開始就選擇使用匿名內部類來實現lambdas,這將限制將來lambda實現更改的範圍,以及它們根據將來JVM改進而演進的能力。
  4. 將lambda表達式轉換爲匿名內部類將限制將來可能的優化(例如緩存),由於它們將綁定到匿名內部類字節碼生成機制。

基於以上4點,lambda表達式的實現不能直接在編譯階段就用匿名內部類實現
,而是須要一個穩定的二進制表示,它提供足夠的信息,同時容許JVM在將來採用其餘可能的實現策略。
解決上述解釋的問題,Java語言和JVM工程師決定將翻譯策略的選擇推遲到運行時。Java 7 中引入的新的 invokedynamic 字節碼指令爲他們提供了一種高效實現這一目標的機制。將lambda表達式轉換爲字節碼須要兩個步驟:

  1. 生成 invokedynamic 調用站點 ( 稱爲lambda工廠 ),當調用該站點時,返回一個函數接口實例,lambda將被轉換到該接口;
  2. 將lambda表達式的主體轉換爲將經過invokedynamic指令調用的方法。

爲了演示第一步,讓咱們檢查編譯一個包含lambda表達式的簡單類時生成的字節碼,例如:

import java.util.function.Function;

public class Lambda {
    Function<String, Integer> f = s -> Integer.parseInt(s);
}

這將轉化爲如下字節碼:

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

注意,方法引用的編譯略有不一樣,由於javac不須要生成合成方法,能夠直接引用方法。

如何執行第二步取決於lambda表達式是非捕獲 non-capturing (lambda不訪問定義在其主體外部的任何變量) 仍是捕獲 capturing (lambda訪問定義在其主體外部的變量),好比類成員變量。

非捕獲 lambda簡單地被描述爲一個靜態方法,該方法具備與lambda表達式徹底相同的簽名,並在使用lambda表達式的同一個類中聲明。 例如,上面的Lambda類中聲明的lambda表達式能夠被描述爲這樣的方法,這個方法就在使用了lambda表達式的方法的下面生成。

static Integer lambda$1(String s) {
   return Integer.parseInt(s);
}

捕獲 lambda表達式的狀況要複雜一些,由於捕獲的變量必須與lambda的形式參數一塊兒傳遞給實現lambda表達式主體的方法。在這種狀況下,常見的轉換策略是在lambda表達式的參數以前爲每一個捕獲的變量添加一個額外的參數。讓咱們來看一個實際的例子:

int offset = 100;
Function<String, Integer> f = s -> Integer.parseInt(s) + offset;

能夠生成相應的方法實現:

static Integer lambda$1(int offset, String s) {
    return Integer.parseInt(s) + offset;
}

然而,這種翻譯策略並非一成不變的,由於使用invokedynamic指令可讓編譯器在未來靈活地選擇不一樣的實現策略。例如,能夠將捕獲的值封裝在數組中,或者,若是lambda表達式讀取使用它的類的某些字段,則生成的方法能夠是實例方法,而不是聲明爲靜態方法,從而避免了將這些字段做爲附加參數傳遞的須要。

理論上的性能

第一步:是連接步驟,它對應於上面提到的lambda工廠步驟。若是咱們將性能與匿名內部類進行比較,那麼等效的操做將是裝入匿名內部類。Oracle已經發布了Sergey Kuksenko關於這一權衡的性能分析,您能夠看到Kuksenko在2013年JVM語言峯會[3]上發表了關於這個主題的演講。分析代表,預熱lambda工廠方法須要時間,在此期間,初始化速度較慢。當連接了足夠多的調用站點時,若是代碼處於熱路徑上(即,其中一個頻繁調用,足以編譯JIT)。另外一方面,若是是冷路徑 (cold path),lambda工廠方法能夠快100倍。

第二步是:從周圍範圍捕獲變量。正如咱們已經提到的,若是沒有要捕獲的變量,那麼能夠自動優化此步驟,以免使用基於lambda工廠的實現分配新對象。在匿名內部類方法中,咱們將實例化一個新對象。爲了優化相同的狀況,您必須手動優化代碼,方法是建立一個對象並將其提高到一個靜態字段中。例如:

// Hoisted Function
public static final Function<String, Integer> parseInt = new Function<String, Integer>() {
    public Integer apply(String arg) {
        return Integer.parseInt(arg);
    }
}; 

// Usage:
int result = parseInt.apply(「123」);

第三步:是調用實際的方法。目前,匿名內部類和lambda表達式都執行徹底相同的操做,因此這裏的性能沒有區別。非捕獲lambda表達式的開箱即用性能已經領先於提高的匿名內部類等效性能。捕獲lambda表達式的實現與爲捕獲這些字段而分配匿名內部類的性能相似。

下文將講述lambda表達式的實如今很大程度上執行得很好。雖然匿名內部類須要手工優化來避免分配,可是JVM已經爲咱們優化了這種最多見的狀況(一個lambda表達式沒有捕獲它的參數)。

實測的性能

固然,很容易理解整體性能模型,但在實測中又會是怎樣的?咱們已經在一些軟件項目中使用了Java 8,並取得了良好的效果。自動優化非捕獲lambdas能夠提供很好的好處。有一個特定的例子,它提出了一些關於將來優化方向的有趣問題。

所討論的示例發生在處理系統中使用的一些代碼時,這些代碼須要特別低的GC暫停(理想狀況下是沒有暫停)。所以,最好避免分配太多的對象。該項目普遍使用lambdas來實現回調處理程序。不幸的是,咱們仍然有至關多的回調,在這些回調中,咱們沒有捕獲局部變量,而是但願引用當前類的一個字段,甚至只是調用當前類的一個方法。目前,這彷佛仍然須要分配。

總結

在本文中,咱們解釋了lambdas不只僅是底層的匿名內部類,以及爲何匿名內部類不是lambda表達式的合適實現方法。考慮lambda表達式實現方法已經作了大量工做。目前,對於大多數任務,它們都比匿名內部類更快,但目前的狀況並不完美;測量驅動的手工優化仍有必定的空間。

不過,Java 8中使用的方法不只限於Java自己。Scala從來經過生成匿名內部類來實現它的lambda表達式。在Scala 2.12中,雖然已經開始使用Java 8中引入的lambda元操做機制。隨着時間的推移,JVM上的其餘語言也可能採用這種機制。

相關文章
相關標籤/搜索