問題解決思路:查看編譯生成的字節碼文件html
同步發佈地址:https://www.cnblogs.com/chenj...java
思路一:express
javac fileName.java
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$1
和Strategize$2
,二者是實現方式是相同的,都是實現了Strategy
接口的class
。
到此,能夠說明匿名內部類的實現:第一次編譯的時候經過字節碼工具多生成一個class來實現的。
第一步:修改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$1
和 Strategize$$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 |
下面的譯本,原文Java-8-Lambdas-A-Peek-Under-the-Hood
匿名內部類具備可能影響應用程序性能的不受歡迎的特性。
基於以上4點,lambda表達式的實現不能直接在編譯階段就用匿名內部類實現
,而是須要一個穩定的二進制表示,它提供足夠的信息,同時容許JVM在將來採用其餘可能的實現策略。
解決上述解釋的問題,Java語言和JVM工程師決定將翻譯策略的選擇推遲到運行時。Java 7 中引入的新的 invokedynamic
字節碼指令爲他們提供了一種高效實現這一目標的機制。將lambda表達式轉換爲字節碼須要兩個步驟:
invokedynamic
調用站點 ( 稱爲lambda工廠 ),當調用該站點時,返回一個函數接口實例,lambda將被轉換到該接口;爲了演示第一步,讓咱們檢查編譯一個包含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上的其餘語言也可能採用這種機制。