Java8 Lambda和Invokedynamic詳情
一、闡明lambda
Java8于2014年3月發(fā)布,并引入了lambda
表達式作為其旗艦功能。我們可能已經(jīng)在代碼庫中使用它們來編寫更簡潔、更靈活的代碼。例如,我們可以將lambda
表達式與新的Streams API
結(jié)合起來,以表達豐富的數(shù)據(jù)處理查詢:
int total = invoices.stream() .filter(inv -> inv.getMonth() == Month.JULY) .mapToInt(Invoice::getAmount) .sum();
此示例顯示如何從發(fā)票集合中計算7月份到期的總金額。傳遞lambda
表達式以查找月份為7月的發(fā)票,并傳遞方法引用以從發(fā)票中提取金額。
您可能想知道Java編譯器如何在幕后實現(xiàn)lambda表達式和方法引用,以及Java虛擬機(JVM)如何處理它們。例如,lambda表達式只是匿名內(nèi)部類的語法糖嗎?畢竟,可以通過將lambda表達式的主體復(fù)制到匿名類的相應(yīng)方法的主體中來翻譯上面的代碼
int total = invoices.stream() .filter(new Predicate<Invoice>() { @Override public boolean test(Invoice inv) { return inv.getMonth() == Month.JULY; } }) .mapToInt(new ToIntFunction<Invoice>() { @Override public int applyAsInt(Invoice inv) { return inv.getAmount(); } }) .sum();
本文將解釋為什么Java編譯器不遵循這種機制,并將闡明lambda
表達式和方法引用是如何實現(xiàn)的。我們將研究字節(jié)碼生成,并在實驗室中簡要分析lambda
性能。最后,我們將討論現(xiàn)實世界中的性能影響。
二、匿名內(nèi)部類
匿名內(nèi)部類具有可能影響應(yīng)用程序性能的不良特征。
首先,編譯器為每個匿名內(nèi)部類生成一個新的類文件。文件名通??雌饋硐?code>ClassName$1,其中ClassName
是定義匿名內(nèi)部類的類的名稱,后跟一個美元符號和一個數(shù)字。生成許多類文件是不可取的,因為每個類文件在使用之前都需要加載和驗證,這會影響應(yīng)用程序的啟動性能。加載可能是一項昂貴的操作,包括磁盤I/O和解壓縮JAR文件本身。
如果將lambda
轉(zhuǎn)換為匿名內(nèi)部類,則每個lambda
都會有一個新的類文件。由于每個匿名內(nèi)部類都將被加載,因此它將占用JVM元空間的空間(這是永久生成的Java8
替代品)。如果JVM將每個匿名內(nèi)部類中的代碼編譯成機器代碼,那么它將存儲在代碼緩存中。此外,這些匿名內(nèi)部類將被實例化為單獨的對象。因此,匿名內(nèi)部類會增加應(yīng)用程序的內(nèi)存消耗。引入緩存機制以減少所有這些內(nèi)存開銷可能會有所幫助,這促使引入某種抽象層。
最重要的是,從第一天起選擇使用匿名內(nèi)部類實現(xiàn)lambda
將限制未來lambda
實現(xiàn)更改的范圍,以及它們根據(jù)未來JVM改進而發(fā)展的能力。
讓我們看一下以下代碼:
import java.util.function.Function; public class AnonymousClassExample { Function<String, String> format = new Function<String, String>() { public String apply(String input){ return Character.toUpperCase(input.charAt(0)) + input.substring(1); } }; }
我們可以使用命令檢查為任何類文件生成的字節(jié)碼
javap -c -v ClassName
為作為匿名內(nèi)部類創(chuàng)建的函數(shù)生成的相應(yīng)字節(jié)碼如下所示:
0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: new #2 // class AnonymousClassExample$1 8: dup 9: aload_0 10: invokespecial #3 // Method AnonymousClass$1."<init>":(LAnonymousClassExample;)V 13: putfield #4 // Field format:Ljava/util/function/Function; 16: return
此代碼顯示以下內(nèi)容:
- 5:使用字節(jié)碼操作new實例化匿名類示例$1類型的對象。同時在堆棧上推送對新創(chuàng)建對象的引用。
- 8:dup操作在堆棧上復(fù)制該引用。
- 10:然后,該值由
invokespecial
指令使用,該指令初始化匿名內(nèi)部類實例。 - 13:堆棧頂部現(xiàn)在仍然包含對對象的引用,該引用使用
putfield
指令存儲在AnonymousClassExample
類的format
字段中。
AnonymousClassExample$1
是編譯器為匿名內(nèi)部類生成的名稱。如果您想讓自己放心,還可以檢查AnonymousClassExample$1
類文件,您將找到函數(shù)接口實現(xiàn)的代碼。
將lambda
表達式轉(zhuǎn)換為匿名內(nèi)部類將限制未來可能的優(yōu)化(例如緩存),因為它們與匿名內(nèi)部類字節(jié)碼生成機制相關(guān)聯(lián)。因此,語言和JVM工程師需要一個穩(wěn)定的二進制表示,該表示提供了足夠的信息,同時允許JVM在將來使用其他可能的實現(xiàn)策略。下一節(jié)將解釋這是如何實現(xiàn)的!
三、Lambdas和Invokedynamic
為了解決上一節(jié)中解釋的問題,Java語言和JVM工程師決定將轉(zhuǎn)換策略的選擇推遲到運行時。Java7引入的新invokedynamic
字節(jié)碼指令為他們提供了一種高效實現(xiàn)這一點的機制。lambda
表達式到字節(jié)碼的轉(zhuǎn)換分兩步執(zhí)行:
- 生成一個
invokedynamic
調(diào)用站點(稱為lambda工廠),調(diào)用該站點時,該站點返回lambda
正在轉(zhuǎn)換到的功能接口的實例; - 將
lambda
表達式體轉(zhuǎn)換為將通過invokedynamic
指令調(diào)用的方法。
為了說明第一步,讓我們檢查編譯包含lambda表達式的簡單類時生成的字節(jié)碼,例如:
import java.util.function.Function; public class Lambda { Function<String, Integer> f = s -> Integer.parseInt(s); }
這將轉(zhuǎn)換為以下字節(jié)碼:
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不需要生成合成方法,可以直接引用該方法。
第二步的執(zhí)行方式取決于lambda表達式是非捕獲(lambda
不訪問在其主體外部定義的任何變量)還是捕獲(lambda訪問在其主體外部定義的變量)。
非捕獲lambda
被簡單地分解為一個靜態(tài)方法,該方法具有與lambda
表達式完全相同的簽名,并在使用lambda
表達式的同一類中聲明。例如,可以將上面lambda
類中聲明的lambda
表達式分解為如下方法:
static Integer lambda$1(String s) { return Integer.parseInt(s); }
注意:$1
不是一個內(nèi)部類,它只是我們表示編譯器生成代碼的方式
捕獲lambda
表達式的情況稍微復(fù)雜一些,因為捕獲的變量必須與lambda
的形式參數(shù)一起傳遞給實現(xiàn)lambda
表達式主體的方法。在這種情況下,常見的轉(zhuǎn)換策略是在lambda
表達式的參數(shù)前面加上每個捕獲變量的附加參數(shù)。讓我們看一個實際的例子:
int offset = 100; Function<String, Integer> f = s -> Integer.parseInt(s) + offset;
相應(yīng)的方法實現(xiàn)可以通過asy生成:
static Integer lambda$1(int offset, String s) { return Integer.parseInt(s) + offset; }
然而,這種轉(zhuǎn)換策略并不是一成不變的,因為invokedynamic
指令的使用使編譯器能夠靈活地在將來選擇不同的實現(xiàn)策略。例如,捕獲的值可以裝箱到數(shù)組中,或者,如果lambda
表達式讀取使用它的類的某些字段,則生成的方法可以是實例方法,而不是聲明為靜態(tài)的,從而避免將這些字段作為附加參數(shù)傳遞。
四、性能表現(xiàn)
這種方法的主要優(yōu)點是性能特性。如果把它們看作是可以簡化為一個數(shù)字,那就太好了,但實際上這里涉及到多個操作。
第一步是聯(lián)動步驟,與上述lambda工廠步驟相對應(yīng)。如果我們將性能與匿名內(nèi)部類進行比較,那么等效的操作將是匿名內(nèi)部類的類加載。Oracle
已經(jīng)發(fā)布了Sergey Kuksenko
對這一權(quán)衡的性能分析,您可以看到Kuksenko
在2013年JVM語言峰會上就這一主題發(fā)表了演講[3]。分析表明,需要時間來預(yù)熱lambda
工廠方法,在此過程中,初始速度較慢。當有足夠多的調(diào)用站點鏈接時,如果代碼位于熱路徑上(即調(diào)用頻率足以編譯JIT的路徑),則性能與類加載一致。另一方面,如果是冷路徑,lambda
工廠方法可以快100倍。
第二步是從周圍范圍捕獲變量。正如我們已經(jīng)提到的,如果沒有要捕獲的變量,那么可以自動優(yōu)化此步驟,以避免使用基于lambda
工廠的實現(xiàn)分配新對象。在匿名內(nèi)部類方法中,我們將實例化一個新對象。為了優(yōu)化等效情況,您必須通過創(chuàng)建單個對象并將其提升到靜態(tài)字段來手動優(yōu)化代碼。例如:
// 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”);
第三步是調(diào)用實際方法。目前,匿名內(nèi)部類和lambda
表達式都執(zhí)行完全相同的操作,因此這里的性能沒有差異。非捕獲lambda
表達式的開箱即用性能已經(jīng)領(lǐng)先于提升的匿名內(nèi)部類。捕獲lambda
表達式的實現(xiàn)與分配匿名內(nèi)部類以捕獲這些字段的性能類似。
我們在本節(jié)中看到,lambda
表達式的實現(xiàn)大體上表現(xiàn)良好。雖然匿名內(nèi)部類需要手動優(yōu)化以避免分配,但JVM已經(jīng)為我們優(yōu)化了最常見的情況(一個不捕獲其參數(shù)的lambda表達式)。
當然,理解整體性能模型是很好的,但是在實踐中,事情是如何疊加的呢?我們已經(jīng)在一些軟件項目中使用了Java8,并取得了積極的成果。自動優(yōu)化非捕獲lambda
可以提供很好的好處。這里有一個特別的例子,它提出了一些關(guān)于未來優(yōu)化方向的有趣問題。
所討論的示例發(fā)生在處理某些代碼以供系統(tǒng)使用時,該系統(tǒng)需要特別低的GC暫停,理想情況下沒有。因此,希望避免分配太多的對象。該項目廣泛使用lambdas來實現(xiàn)回調(diào)處理程序。不幸的是,我們?nèi)匀挥邢喈敹嗟幕卣{(diào),其中我們沒有捕獲局部變量,但希望引用當前類的字段,甚至只調(diào)用當前類上的方法。目前,這似乎仍然需要分配。下面是一個代碼示例,旨在闡明我們所討論的內(nèi)容:
public MessageProcessor() {} public int processMessages() { return queue.read(obj -> { if (obj instanceof NewClient) { this.processNewClient((NewClient) obj); } ... }); }
這個問題有一個簡單的解決辦法。我們將代碼提升到構(gòu)造函數(shù)中,并將其分配給一個字段,然后在調(diào)用站點直接引用該字段。下面是我們之前重寫的代碼示例:
private final Consumer<Msg> handler; public MessageProcessor() { handler = obj -> { if (obj instanceof NewClient) { this.processNewClient((NewClient) obj); } ... }; } public int processMessages() { return queue.read(handler); }
在所討論的項目中,這是一個嚴重的問題:內(nèi)存分析顯示,此模式負責前八個對象分配站點中的六個,以及應(yīng)用程序總分配的60%以上。
與任何潛在的優(yōu)化一樣,無論環(huán)境如何,應(yīng)用這種方法都可能會帶來其他問題。
您選擇編寫非慣用代碼純粹是出于性能原因。因此有一個可讀性權(quán)衡
這也關(guān)系到分配的權(quán)衡。您正在向MessageProcessor
添加一個字段,使其更大,以便分配。相關(guān)lambda
的創(chuàng)建和捕獲也會減慢對MessageProcessor
的構(gòu)造函數(shù)調(diào)用。
我們不是通過尋找場景,而是通過內(nèi)存分析發(fā)現(xiàn)了這種情況,并且有一個很好的業(yè)務(wù)用例證明了優(yōu)化的合理性。我們還處于這樣一個位置:對象只分配一次,大量重用lambda
表達式,因此緩存非常有益。與任何性能調(diào)整練習(xí)一樣,通常推薦使用科學(xué)方法。
這也是任何其他最終用戶尋求優(yōu)化其lambda
表達式使用的方法。嘗試編寫干凈、簡單且功能強大的代碼始終是最好的第一步。任何優(yōu)化,如本次吊裝,應(yīng)僅針對真正的問題進行。編寫捕獲分配對象的lambda
表達式本身并不壞——正如編寫調(diào)用'new Foo()
'的Java
代碼本身也不壞一樣。
這一經(jīng)驗也確實表明,要充分利用lambda
表達式,重要的是要習(xí)慣地使用它們。如果lambda
表達式用于表示小的純函數(shù),則它們幾乎不需要從其周圍范圍捕獲任何內(nèi)容。和大多數(shù)事情一樣,如果你保持簡單,事情就會表現(xiàn)得很好。
結(jié)論
在本文中,我們解釋了lambda不僅僅是隱藏的匿名內(nèi)部類,以及為什么匿名內(nèi)部類不是lambda
表達式的合適實現(xiàn)方法。通過lambda
表達式實現(xiàn)方法,已經(jīng)進行了大量的工作。目前,對于大多數(shù)任務(wù),它們都比匿名內(nèi)部類快,但當前的狀態(tài)并不完美;測量驅(qū)動的手動優(yōu)化仍有一定的空間。
Java8中使用的方法不僅僅局限于Java本身。Scala歷來通過生成匿名內(nèi)部類來實現(xiàn)其lambda
表達式。在Scala2.12中,我們已經(jīng)開始使用Java8中引入的lambda
元工廠機制。隨著時間的推移,JVM上的其他語言也可能采用這種機制。
到此這篇關(guān)于Java8 Lambda
和Invokedynamic
詳情的文章就介紹到這了,更多相關(guān)Java8 Lambda
和Invokedynamic
內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot框架中如何整合mybatis框架思路詳解
這篇文章主要介紹了springboot框架中如何整合mybatis框架,本文通過示例圖文相結(jié)合給大家介紹的非常詳細,需要的朋友可以參考下2022-12-12SpringMVC結(jié)合ajaxfileupload.js實現(xiàn)文件無刷新上傳
這篇文章主要介紹了SpringMVC結(jié)合ajaxfileupload.js實現(xiàn)文件無刷新上傳,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-10-10如何解決線程太多導(dǎo)致java socket連接池出現(xiàn)的問題
這篇文章主要介紹了如何解決線程太多導(dǎo)致socket連接池出現(xiàn)的問題,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-12-12Java中如何將符號分隔的文本文件txt轉(zhuǎn)換為excel
這篇文章主要介紹了Java中如何將符號分隔的文本文件txt轉(zhuǎn)換為excel,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-09-09SpringBoot整合HikariCP數(shù)據(jù)庫連接池方式
這篇文章主要介紹了SpringBoot整合HikariCP數(shù)據(jù)庫連接池方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-03-03SpringBoot自動裝配Condition的實現(xiàn)方式
這篇文章主要介紹了SpringBoot自動裝配Condition的實現(xiàn)方式,本文通過實例代碼給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2021-08-08