成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

淺析JVM invokedynamic指令和Java Lambda語法

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

一、導語

盡管近年來JDK的版本發布愈發敏捷,當前最新版本號已經20+,但是日常使用中,JDK8還是占據了統治地位。

圖片圖片

你發任你發,我用Java8:【Jetbrains】2023 開發者生態系統現狀 - https://www.jetbrains.com/zh-cn/lp/devecosystem-2023/java/

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

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

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

二、Java和JVM的關系

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

Java是基于JVM提供的虛擬指令集,設計、實現的一種供開發者使用的高級語言。通過配套的編譯器和標準庫,將文本格式的Java代碼編譯成符合JVM指令集規范的二進制文件,交付到JVM執行。

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

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

圖片圖片

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

三、JVM指令:invokedynamic

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

圖片圖片

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

圖片圖片

圖片圖片

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

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

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

偽代碼對比如下:

圖片圖片

MethdoHandle為示意,后文有詳述。

偽字節碼偽字節碼

invokevirtual指令直接調用目標方法,invokedynamic直接調用回調方法,再調用回調方法返回的方法句柄。

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

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

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

一階段:調用引導方法確定并緩存CallSite(MethodHandle)

二階段:調用CallSite(MethodHandle)

字節碼指令比較low level,除字節碼業務插樁場景外,字節碼指令序列的構造、編排一般都由【高級語言編譯器】來根據語言語法規則自動完成,如javac。

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

四、方法句柄:MethodHandle

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

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

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

圖片圖片

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

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

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

五、Lambda表達式簡介

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

當【匿名內部類】匿名實現的是一個FunctionalInterface時,可以用Lambda表達式平替。

示例如下:

圖片圖片

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

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

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

圖片圖片

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

圖片圖片

圖片圖片

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

六、Lambda表達式實現

Lambda表達式具體的實現涉及類文件結構、字節碼指令結構、標準庫等多個方面的內容,千頭萬緒。也想不出來什么通俗易懂的敘述角度,只能是枯燥的對照著字節碼分析了。

圖片圖片

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

圖片圖片

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表達式對應3條invokedynamic指令:

圖片圖片

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

invokedynamic指令參數

invokedynamic指令參數結構如下:

圖片圖片

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

invokedynamic指令需要指定其期待BSM返回的方法特征(出入參類型)和BSM方法引用。該參數以CONSTANT_InvokeDynamic_info結構存放在類文件的常量池結構中,invokedynamic用兩個byte寬度的常量池索引號指定。

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

圖片圖片

對照字節碼我們可知,Lambda1相關的invokedynamic指定的CONSTANT_InvokeDynamic_info序號為3,得到如下內容:

圖片圖片

圖片圖片

期望的方法名稱和描述符

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

IntUnaryOperator anyName();

沒有入參,返回值類型為IntUnaryOperator的MethodHandle。

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

圖片圖片

換句話說,該invokedynamic指令希望相應的BSM返回一個IntUnaryOperator的工廠方法句柄,然后invokedynamic指令再調用這個方法句柄,創建出一個map方法需要的IntUnaryOperator類型的參數。

BSM方法序號

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

通俗來說,類文件中有一個數組,數組名稱叫BootstrapMethods。其結構如下:

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數組中的第一個BSM。

圖片圖片

BSM方法

圖片圖片

圖片圖片

BSM方法參數

該BSM數據結構指定了3個編譯期固定的、靜態的BSM方法參數:

圖片圖片

第一、第三個參數指定了預期的函數式接口(FunctionInterface)的特征:入參為int、出參為int。即上述IntUnaryOperator。

圖片圖片

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

圖片圖片

圖片圖片

至此,invokedynamic指令具有的發起【一階段調用】的上下文如下:

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

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

LambdaMetafactory#metafactory

圖片圖片

如上述,invokedynamic指令調用上述metafactory方法,對照字節碼信息,可以得到如下具體參數表格:

圖片圖片

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

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

構造CallSite

圖片圖片

java.lang.invoke.InnerClassLambdaMetafactory#buildCallSite

生成FunctionalInterface適配類后,基于適配類創建MethodHandle。該MethodHandle體現的代碼邏輯類似如下Java代碼:

圖片圖片

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

二階段調用

【一階段調用】已經完成,返回了動態決策產生的CallSite對象,getTarget方法可以獲取上述的IntUnaryOperator適配類的【工廠方法】。

圖片圖片

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

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

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

七、Lambda表達式性能

圖片圖片

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

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

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

八、Lambda表達式和final變量

圖片圖片

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

假設沒有這個限制,那么Lambda表達式就變成了重構不友好的【位置相關】的代碼塊。

換句話說,下面兩種代碼執行結果是不一樣的:

圖片圖片

Lambda捕獲的num的值為5;

圖片圖片

Lambda捕獲的num的值為3;

如果沒有類似的編譯約束,當我們有心或無意的在復雜的業務邏輯中進行了類似的代碼調整時,極易出錯且難以排查。

九、總結

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

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

本文使用的反編譯工具為: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虛擬機規范(JDK8)--https://docs.oracle.com/javase/specs/jvms/se8/html/
  • Oracle-Java語言規范(JDK8)-https://docs.oracle.com/javase/specs/jls/se8/html/index.html
  • JVM系列之:JVM是怎么實現invokedynamic的? | HeapDump性能社區-https://heapdump.cn/article/3573623
  • Java 虛擬機:JVM是怎么實現invokedynamic的?(上)-https://cloud.tencent.com/developer/article/1787369
  • Java 虛擬機:JVM是怎么實現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表達式和方法引用本質上有什么區別?-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是怎么實現invokedynamic的?(下)-深入拆解Java虛擬機-極客時間-https://time.geekbang.org/column/article/12574
責任編輯:武曉燕 來源: 得物技術
相關推薦

2023-02-15 19:07:30

Java字節碼指令

2009-09-17 09:09:50

Lambda表達式Linq查詢

2021-06-27 06:25:14

代碼優化技巧Java

2016-09-07 20:56:24

2009-09-14 13:44:14

Lambda ExprC# Lambda

2013-03-29 11:09:17

JVM內存

2009-08-27 11:43:31

C#語法

2009-08-14 00:30:09

C#條件編譯指令

2010-09-30 15:19:33

2009-08-18 12:52:33

C#枚舉類型

2015-04-09 10:18:21

網卡配置

2023-09-01 08:59:57

2020-05-14 10:47:13

Android Java 8

2013-01-05 02:19:50

JavaLambda表達式JVM

2010-09-07 10:33:04

CSS

2009-07-06 12:49:33

JSP編譯器

2009-07-02 11:34:42

JSP指令JSP開發

2009-09-17 09:20:45

C#操作XML

2010-01-21 09:34:57

C++語法

2009-07-27 13:34:15

ASP.NET編程
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 欧美女优在线观看 | 亚洲狠狠爱| www.五月婷婷.com | 亚洲精品黄 | 欧美一级片中文字幕 | 青青草在线视频免费观看 | 国产在线观 | 日韩美女在线看免费观看 | 欧美精品三区 | 久热久热 | 亚洲精品一区二区三区在线 | 国产欧美精品一区二区色综合朱莉 | 欧美久久久久久 | 第一福利社区1024 | 日本黄色影片在线观看 | 在线视频成人 | 国产 欧美 日韩 一区 | 久久久人成影片一区二区三区 | 91中文视频 | а√中文在线8 | 色综合一区二区 | 精品国产乱码久久久久久蜜退臀 | 国产91丝袜在线播放 | 久久国产精品72免费观看 | 日日夜夜精品免费视频 | 欧美国产日韩一区二区三区 | wwwxxx国产| 久久99精品久久久97夜夜嗨 | 日日干夜夜操天天操 | 综合伊人 | 日韩视频在线一区 | 99久久国产精 | 精品一二三区视频 | a级毛片免费高清视频 | 一区二区精品在线 | 伊人色综合久久天天五月婷 | 亚洲视频精品 | 男人的天堂亚洲 | 久久久久久免费观看 | 成人精品在线观看 | 日韩欧美一区二区三区免费观看 |