自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

淺析JVM invokedynamic指令和Java Lambda語法

開發(fā) 前端
Lambda表達式語言特性引入Java語言后,賦予了Java語言更便捷的函數(shù)式編程魔力(相對匿名內(nèi)部類),同時也讓其更簡潔,畢竟Java代碼寫起來啰嗦這點一直被開發(fā)者們廣泛詬病。

一、導(dǎo)語

盡管近年來JDK的版本發(fā)布愈發(fā)敏捷,當(dāng)前最新版本號已經(jīng)20+,但是日常使用中,JDK8還是占據(jù)了統(tǒng)治地位。

圖片圖片

你發(fā)任你發(fā),我用Java8:【Jetbrains】2023 開發(fā)者生態(tài)系統(tǒng)現(xiàn)狀 - https://www.jetbrains.com/zh-cn/lp/devecosystem-2023/java/

JDK8如此旺盛的生命力,與其優(yōu)異的兼容性、穩(wěn)定性和足夠日常開發(fā)使用的語言特性有極大的關(guān)系,這其中最引人矚目的語言特性莫過于Lambda表達式。

Lambda表達式語言特性引入Java語言后,賦予了Java語言更便捷的函數(shù)式編程魔力(相對匿名內(nèi)部類),同時也讓其更簡潔,畢竟Java代碼寫起來啰嗦這點一直被開發(fā)者們廣泛詬病。

本文將從JVM和Java兩個層面著手,和大家一起深入解析Lambda表達式。

二、Java和JVM的關(guān)系

JVM是HLLVM(高級語言虛擬機),其參考物理計算機體系架構(gòu),設(shè)計、實現(xiàn)了一套特定領(lǐng)域虛擬指令集,即:字節(jié)碼指令。利用上述虛擬指令集作為中間層,將上層高級語言和底層體系架構(gòu)解耦以規(guī)避繁瑣、復(fù)雜的平臺兼容性問題,以實現(xiàn)【一次編譯,處處運行】。

Java是基于JVM提供的虛擬指令集,設(shè)計、實現(xiàn)的一種供開發(fā)者使用的高級語言。通過配套的編譯器和標(biāo)準(zhǔn)庫,將文本格式的Java代碼編譯成符合JVM指令集規(guī)范的二進制文件,交付到JVM執(zhí)行。

Java是一種運行在JVM平臺上的高級語言,但是JVM平臺絕不是只能運行Java語言。任何人都可以設(shè)計自己的語言語法,只要能按JVM規(guī)范編譯成合法的JVM字節(jié)碼,即可在JVM上運行(用Java命令)。

計算機科學(xué)領(lǐng)域的任何問題,都可以通過增加一個中間層來解決。

圖片圖片

沒有無源之水,Java語言層面的特性,除非是純語法糖,不然一定離不開特定JVM特性的支撐。Lambda是Java8語言特性,那支撐它的便是JVM invokedynamic指令。

三、JVM指令:invokedynamic

在Java7之前,JVM提供了如下4種【方法調(diào)用】指令:

圖片圖片

上述4種字節(jié)碼指令各自有不同的使用場景,但是有一個共同的特點:目標(biāo)方法一定需要在【編譯期】確定。如下圖,編譯后4種指令的參數(shù)都指定了目標(biāo)方法所在的類和簽名以供運行時鏈接、動態(tài)分派。

圖片圖片

圖片圖片

這個特點一方面保證了JVM語言類型安全,另一方面也限制了JVM平臺對動態(tài)類型高級語言的支持。比如想讓JavaScript、Python等動態(tài)語言代碼編譯成JVM字節(jié)碼運行在JVM平臺上的開銷會比較大,性能也會比較差。

為了解決上述問題, Java7引入了一條新的虛擬機指令:invokedynamic。這是自JVM 1.0以來第一次引入新的虛擬機指令,invokedynamic與其他 invoke*指令不同的是它允許由應(yīng)用級的代碼來決定方法解析(鏈接、分派)。

所謂的【應(yīng)用級的代碼來決定方法解析】需要對照之前的invoke*指令來理解。之前的4種invoke*指令,在編譯期就必須要明確目標(biāo)方法并hardcode到字節(jié)碼中,JVM在運行時直接解析、鏈接、動態(tài)分派硬編碼指定的目標(biāo)方法。而invokedynamic指令通過回調(diào)機制來獲取需要調(diào)用的目標(biāo)方法。即先調(diào)用業(yè)務(wù)自定義回調(diào)方法做方法決策(解析、鏈接),再調(diào)用其返回的目標(biāo)方法。筆者稱之為【兩階段調(diào)用】。

偽代碼對比如下:

圖片圖片

MethdoHandle為示意,后文有詳述。

偽字節(jié)碼偽字節(jié)碼

invokevirtual指令直接調(diào)用目標(biāo)方法,invokedynamic直接調(diào)用回調(diào)方法,再調(diào)用回調(diào)方法返回的方法句柄。

傳統(tǒng)的invoke*指令直接調(diào)用字節(jié)碼中指定的目標(biāo)方法,如Son.testMethod1,invokedynamic指令在調(diào)用時,先調(diào)用字節(jié)碼中指定的回調(diào)方法,如Son.dynamicMethodCallback,然后再調(diào)用回調(diào)方法(hook)返回的方法引用。

而上述dynamicMethodCallback即為【應(yīng)用級的代碼或者我們常說的業(yè)務(wù)代碼】,可以在不影響性能的前提下,靈活的干預(yù)JVM方法解析、鏈接的過程。

總結(jié)來說,所謂應(yīng)用級的代碼其實也是一個方法,在這里這個方法被稱為引導(dǎo)方法(Bootstrap Method),簡稱 BSM。invokedynamic執(zhí)行時,BSM先被調(diào)用并返回一個 CallSite(調(diào)用點)對象,這個對象就和 invokedynamic鏈接在一起。以后再執(zhí)行這條invokedynamic指令都不會創(chuàng)建新的 CallSite 對象。CallSite就是一個 MethodHandle(方法句柄)的holder,方法句柄指向一個調(diào)用點真正執(zhí)行的方法。

一階段:調(diào)用引導(dǎo)方法確定并緩存CallSite(MethodHandle)

二階段:調(diào)用CallSite(MethodHandle)

字節(jié)碼指令比較low level,除字節(jié)碼業(yè)務(wù)插樁場景外,字節(jié)碼指令序列的構(gòu)造、編排一般都由【高級語言編譯器】來根據(jù)語言語法規(guī)則自動完成,如javac。

某種意義上有點類似Java【動態(tài)代理】機制,都是通過調(diào)用橫切來動態(tài)橋接、靈活決策目標(biāo)方法。

四、方法句柄:MethodHandle

前面我們知道invokedynamic指令支持通過業(yè)務(wù)層面自定義的BSM來靈活的決策被調(diào)用的目標(biāo)方法,也就是上述的【一階段】。BSM方法的返回值就是【二階段】調(diào)用的方法。

但是和C、Python等語言不同,Java中方法/函數(shù)不是一等公民,也就是在Java中無法將【方法變量】作為方法返回值。

為了解決這個問題,Java標(biāo)準(zhǔn)庫提供了一個新的類型MethodHandle,用于實現(xiàn)類似C語言中的方法指針、JavaScript/Python中方法變量的能力。該API和反射API呈現(xiàn)的能力相似,但是性能更好。

圖片圖片

上述為MethodHandle API的基本使用,該課題展開又是一篇長文。總之,我們可以用MethodHandle來作為【方法變量】,變相的將【Java方法】提升為【一等公民】,從而可以在BSM中用Java代碼實現(xiàn)動態(tài)編排、決策,返回合適的方法指針。這也是上述invokedynamic+BSM機制能夠成立的一個基礎(chǔ)。

詳見:秒懂Java之方法句柄(MethodHandle) (https://blog.csdn.net/ShuSheng0007/article/details/107066856)

上述【一階段】調(diào)用的本質(zhì)就是得到一個特定的MethodHandle(方法指針/方法引用),【二階段】調(diào)用就是調(diào)用這個MethodHandle。

五、Lambda表達式簡介

Java的Lambda表達式,是傳統(tǒng)的【匿名內(nèi)部類】特性在特定場景下的平替特性。所謂的特定場景,即我們熟知的FunctionalInterface。

當(dāng)【匿名內(nèi)部類】匿名實現(xiàn)的是一個FunctionalInterface時,可以用Lambda表達式平替。

示例如下:

圖片圖片

函數(shù)式接口(Functional Interface)就是一個有且僅有一個抽象方法,但是可以有多個非抽象方法的接口。

Java 不會強制要求你使用 @FunctionalInterface 注解來標(biāo)記你的接口是函數(shù)式接口,然而,作為API作者,你可能傾向使用@FunctionalInterface指明特定的接口為函數(shù)式接口,這只是一個設(shè)計上的考慮,可以讓用戶很明顯的知道一個接口是函數(shù)式接口。

Java Lambda表達式在語法層面有兩種形式:行內(nèi)代碼塊、方法引用。

圖片圖片

但是在編譯產(chǎn)物中,行內(nèi)Lambda最終會被提取到獨立的靜態(tài)方法中。也就是說,在字節(jié)碼層面只有【方法引用】一種Lambda形式。

圖片圖片

圖片圖片

如上圖反編譯結(jié)果,兩個行內(nèi)Lambda中的代碼在編譯后被提取到兩個自動生成的方法lambda$main$0、lambda$main$1,后續(xù)Lambda表達式的處理流程都可以收斂,無需區(qū)分對待。

六、Lambda表達式實現(xiàn)

Lambda表達式具體的實現(xiàn)涉及類文件結(jié)構(gòu)、字節(jié)碼指令結(jié)構(gòu)、標(biāo)準(zhǔn)庫等多個方面的內(nèi)容,千頭萬緒。也想不出來什么通俗易懂的敘述角度,只能是枯燥的對照著字節(jié)碼分析了。

圖片圖片

如上圖,mian方法中聲明了3個Lambda表達式,反編譯字節(jié)碼可以看到字節(jié)碼指令流如下:

圖片圖片

0 iconst_3
 1 istore_1
 2 iconst_3
 3 newarray 10 (int)
 5 dup
 6 iconst_0
 7 iconst_1
 8 iastore
 9 dup
10 iconst_1
11 iconst_2
12 iastore
13 dup
14 iconst_2
15 iconst_3
16 iastore
17 invokestatic #2 <java/util/stream/IntStream.of : ([I)Ljava/util/stream/IntStream;>
20 invokedynamic #3 <applyAsInt, BootstrapMethods #0>
25 invokeinterface #4 <java/util/stream/IntStream.map : (Ljava/util/function/IntUnaryOperator;)Ljava/util/stream/IntStream;> count 2
30 iload_1
31 invokedynamic #5 <applyAsInt, BootstrapMethods #1>
36 invokeinterface #4 <java/util/stream/IntStream.map : (Ljava/util/function/IntUnaryOperator;)Ljava/util/stream/IntStream;> count 2
41 invokedynamic #6 <applyAsInt, BootstrapMethods #2>
46 invokeinterface #4 <java/util/stream/IntStream.map : (Ljava/util/function/IntUnaryOperator;)Ljava/util/stream/IntStream;> count 2
51 invokeinterface #7 <java/util/stream/IntStream.sum : ()I> count 1
56 istore_2
57 return

3個lambda表達式對應(yīng)3條invokedynamic指令:

圖片圖片

第一個lambda表達式比較簡單且典型,后續(xù)我們以其為抓手展開分析。

invokedynamic指令參數(shù)

invokedynamic指令參數(shù)結(jié)構(gòu)如下:

圖片圖片

jvms-6.5.invokedynamic (https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.invokedynamic)

invokedynamic指令需要指定其期待BSM返回的方法特征(出入?yún)㈩愋?和BSM方法引用。該參數(shù)以CONSTANT_InvokeDynamic_info結(jié)構(gòu)存放在類文件的常量池結(jié)構(gòu)中,invokedynamic用兩個byte寬度的常量池索引號指定。

CONSTANT_InvokeDynamic_info {
    u1 tag;
    u2 bootstrap_method_attr_index;
    u2 name_and_type_index;
}

圖片圖片

對照字節(jié)碼我們可知,Lambda1相關(guān)的invokedynamic指定的CONSTANT_InvokeDynamic_info序號為3,得到如下內(nèi)容:

圖片圖片

圖片圖片

期望的方法名稱和描述符

該invokedynamic指令期望BSM0方法返回一個如下特征的方法引用:

IntUnaryOperator anyName();

沒有入?yún)ⅲ祷刂殿愋蜑镮ntUnaryOperator的MethodHandle。

為什么是返回IntUnaryOperator類型呢?因為IntStream的map方法需要的參數(shù)是IntUnaryOperator類型。

圖片圖片

換句話說,該invokedynamic指令希望相應(yīng)的BSM返回一個IntUnaryOperator的工廠方法句柄,然后invokedynamic指令再調(diào)用這個方法句柄,創(chuàng)建出一個map方法需要的IntUnaryOperator類型的參數(shù)。

BSM方法序號

BSM方法序號指定了當(dāng)前invokedynamic指令使用的BSM方法在BSM方法表中的索引。

通俗來說,類文件中有一個數(shù)組,數(shù)組名稱叫BootstrapMethods。其結(jié)構(gòu)如下:

BootstrapMethods_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 num_bootstrap_methods;
    {   u2 bootstrap_method_ref;
        u2 num_bootstrap_arguments;
        u2 bootstrap_arguments[num_bootstrap_arguments];
    } bootstrap_methods[num_bootstrap_methods];
}

圖片圖片

圖片圖片

圖片圖片

該invokedynamic指令指定的BSM為BSM數(shù)組中的第一個BSM。

圖片圖片

BSM方法

圖片圖片

圖片圖片

BSM方法參數(shù)

該BSM數(shù)據(jù)結(jié)構(gòu)指定了3個編譯期固定的、靜態(tài)的BSM方法參數(shù):

圖片圖片

第一、第三個參數(shù)指定了預(yù)期的函數(shù)式接口(FunctionInterface)的特征:入?yún)閕nt、出參為int。即上述IntUnaryOperator。

圖片圖片

第二個參數(shù)是一個靜態(tài)方法引用。如上述,Lambda表達式在編譯時會被提取到一個自動生成的方法中。

圖片圖片

圖片圖片

至此,invokedynamic指令具有的發(fā)起【一階段調(diào)用】的上下文如下:

  1. 具體的一階段調(diào)用的BSM方法:java.lang.invoke.LambdaMetafactory#metafactory
  2. IntStream.map方法需要的參數(shù)類型:IntUnaryOperator
  3. 編譯器(javac)編譯產(chǎn)生的包含Lambda表達式代碼內(nèi)容的靜態(tài)方法:lambda$main$0(I)I

接下來就是調(diào)用java.lang.invoke.LambdaMetafactory#metafactory方法,傳遞上述必要的上下文參數(shù),接受metafactory方法返回的IntUnaryOperator applyAsInt()類型的MethodHandle并調(diào)用該MethodHandle,繼而得到IntStream.map方法需要的參數(shù):IntUnaryOperator。

LambdaMetafactory#metafactory

圖片圖片

如上述,invokedynamic指令調(diào)用上述metafactory方法,對照字節(jié)碼信息,可以得到如下具體參數(shù)表格:

圖片圖片

LambdaMetafactory根據(jù)上述上下文,使用ASM庫,動態(tài)生成了一個如下所示的IntUnaryOperator適配類,用于橋接Lambda表達式代碼塊到IntUnaryOperator類型。

添加-Djdk.internal.lambda.dumpProxyClasses=.啟動參數(shù),JDK會將生成的適配函數(shù)式接口的類源碼輸出到工作目錄中。

構(gòu)造CallSite

圖片圖片

java.lang.invoke.InnerClassLambdaMetafactory#buildCallSite

生成FunctionalInterface適配類后,基于適配類創(chuàng)建MethodHandle。該MethodHandle體現(xiàn)的代碼邏輯類似如下Java代碼:

圖片圖片

至此,invokedynamic【一階段】調(diào)用已經(jīng)完成,invokedynamic指令獲取到了由LambdaMetafactory#metafactory作為BSM動態(tài)決策、動態(tài)生成的IntUnaryOperator適配類的【工廠方法】(以CallSite包裝的MethodHandle的形式)。

二階段調(diào)用

【一階段調(diào)用】已經(jīng)完成,返回了動態(tài)決策產(chǎn)生的CallSite對象,getTarget方法可以獲取上述的IntUnaryOperator適配類的【工廠方法】。

圖片圖片

至此,invokedynamic指令可以通過如下偽代碼,創(chuàng)建IntStream.map方法需要的IntUnaryOperator實例。

IntUnaryOperator intUnaryOperator = (IntUnaryOperator)callSite.getTarget().invoke()

Lambda1的整個運行時解析、鏈接流程完成。

七、Lambda表達式性能

圖片圖片

經(jīng)過上述分析我們可以知道,Lambda1這種無狀態(tài)的、沒有捕獲外部變量(閉包)的Lambda表達式的開銷是很小的,只會在第一次調(diào)用時動態(tài)生成橋接的適配類,實例化后就通過ConstantCallSite緩存。后續(xù)所有的調(diào)用都不會再重新生成適配類、實例化適配類。

但是,Lambda2則不同,因為Lambda捕獲、依賴了(閉包)外部變量num,那么這個表達式就是有狀態(tài)的。雖然同樣只是會在第一次調(diào)用時動態(tài)生成橋接的適配類,但是每一次調(diào)用都會使用num變量重新實例化一個新的適配類實例。這種場景下,其在性能和形式上就已經(jīng)和傳統(tǒng)的【匿名內(nèi)部類】沒有太大差別了。

Lambda3本質(zhì)上和Lambda1一樣,只不過不需要Java編譯器在編譯時將Lambda代碼語句抽取成獨立的方法。

八、Lambda表達式和final變量

圖片圖片

當(dāng)Lambda表達式閉包捕獲的局部變量num在方法內(nèi)可變時,編譯器會提示編譯錯誤。這不是JVM的限制,而是Java語言層面的限制。筆者認為,這種限制沒有技術(shù)上的原因,而是Java語言設(shè)計者刻意的借助編譯器在阻止你犯錯。

假設(shè)沒有這個限制,那么Lambda表達式就變成了重構(gòu)不友好的【位置相關(guān)】的代碼塊。

換句話說,下面兩種代碼執(zhí)行結(jié)果是不一樣的:

圖片圖片

Lambda捕獲的num的值為5;

圖片圖片

Lambda捕獲的num的值為3;

如果沒有類似的編譯約束,當(dāng)我們有心或無意的在復(fù)雜的業(yè)務(wù)邏輯中進行了類似的代碼調(diào)整時,極易出錯且難以排查。

九、總結(jié)

提筆的時候立意高遠,想著要盡可能通俗詳盡的寫清楚所有涉及的技術(shù)點,但是越寫越覺得事情不簡單,最后只能是把博客標(biāo)題從【深入剖析】修改為【淺析】。這塊內(nèi)容牽涉的面太廣,筆者沒有能力也沒有精力介紹到事無巨細、面面俱到,只能為大家拋磚引玉,大家可以配合后文【參考資料】多梳理、多實驗,同時在評論區(qū)批評指正。

  1. invokedynamic指令不是業(yè)務(wù)開發(fā)者使用的。invokedynamic指令可以用來實現(xiàn)Lambda語法,但是它不是只能用來實現(xiàn)Lambda語法。這個指令對于JVM語言開發(fā)者比如Kotlin、Groovy、JRuby、Jython等會比較重要。
  2. 沒有捕獲外部變量(閉包)的Lambda表達式性能和直接調(diào)用沒有差別。
  3. 捕獲外部變量(閉包)的Lambda表達式性能理論上和【匿名內(nèi)部類】范式一樣,每次調(diào)用都會創(chuàng)建一個對象(最壞情況)。

本文使用的反編譯工具為:jclasslib Bytecode Viewer

(https://plugins.jetbrains.com/plugin/9248-jclasslib-bytecode-viewer)

十、附錄

自動生成的Lambda2適配類

// $FF: synthetic class
final class LambdaTest$$Lambda$2 implements IntUnaryOperator {
    private final int arg$1;


    private LambdaTest$$Lambda$2(int var1) {
        this.arg$1 = var1;
    }


    private static IntUnaryOperator get$Lambda(int var0) {
        return new LambdaTest$$Lambda$2(var0);
    }


    @Hidden
    public int applyAsInt(int var1) {
        return LambdaTest.lambda$main$1(this.arg$1, var1);
    }
}

自動生成的Lambda3適配類

// $FF: synthetic class
final class LambdaTest$$Lambda$3 implements IntUnaryOperator {
    private LambdaTest$$Lambda$3() {
    }


    @Hidden
    public int applyAsInt(int var1) {
        return LambdaTest.add(var1);
    }
}

參考:

  • Oracle-Java虛擬機規(guī)范(JDK8)--https://docs.oracle.com/javase/specs/jvms/se8/html/
  • Oracle-Java語言規(guī)范(JDK8)-https://docs.oracle.com/javase/specs/jls/se8/html/index.html
  • JVM系列之:JVM是怎么實現(xiàn)invokedynamic的? | HeapDump性能社區(qū)-https://heapdump.cn/article/3573623
  • Java 虛擬機:JVM是怎么實現(xiàn)invokedynamic的?(上)-https://cloud.tencent.com/developer/article/1787369
  • Java 虛擬機:JVM是怎么實現(xiàn)invokedynamic的?(下)-https://cloud.tencent.com/developer/article/1787371
  • 【stackoverflow】What is a bootstrap method?-https://stackoverflow.com/questions/30733557/what-is-a-bootstrap-method
  • Java中普通lambda表達式和方法引用本質(zhì)上有什么區(qū)別?-https://www.zhihu.com/question/51491241/answer/126232275
  • 理解 invokedynamic-https://juejin.cn/post/6844903503236710414
  • https://www.cnblogs.com/wade-luffy/p/6058087.html
  • 09 | JVM是怎么實現(xiàn)invokedynamic的?(下)-深入拆解Java虛擬機-極客時間-https://time.geekbang.org/column/article/12574
責(zé)任編輯:武曉燕 來源: 得物技術(shù)
相關(guān)推薦

2023-02-15 19:07:30

Java字節(jié)碼指令

2009-09-17 09:09:50

Lambda表達式Linq查詢

2021-06-27 06:25:14

代碼優(yōu)化技巧Java

2016-09-07 20:56:24

2009-09-14 13:44:14

Lambda ExprC# Lambda

2013-03-29 11:09:17

JVM內(nèi)存

2009-08-27 11:43:31

C#語法

2009-08-14 00:30:09

C#條件編譯指令

2023-09-01 08:59:57

2010-09-30 15:19:33

2009-08-18 12:52:33

C#枚舉類型

2013-01-05 02:19:50

JavaLambda表達式JVM

2020-05-14 10:47:13

Android Java 8

2015-04-09 10:18:21

網(wǎng)卡配置

2009-07-06 12:49:33

JSP編譯器

2010-09-07 10:33:04

CSS

2009-07-02 11:34:42

JSP指令JSP開發(fā)

2009-09-17 09:20:45

C#操作XML

2009-07-27 13:34:15

ASP.NET編程

2010-09-27 13:09:29

JVM指令系統(tǒng)
點贊
收藏

51CTO技術(shù)棧公眾號