通過位元組碼分析JDK8中Lambda運算式編譯及執行機制__jvm

來源:互聯網
上載者:User

關於Lambda位元組碼相關的文章,很早之前就想寫了,通過Java8的lambda運算式的運用以及rxJava響應式編程架構,使代碼更加簡潔易維護,調用方式更加便捷。本文將介紹JVM中的方法調用相關的位元組碼指令,重點解析JDK7(JSR-292)之後新增的invokedynamic指令給lambda運算式的動態調用特性提供的實現機制,最後再探討一下lambda效能方面的話題。 方法調用的位元組碼指令

在介紹invokedynamic指令前,先回顧一下JVM規範中的所有方法調用的位元組碼指令。其他關於位元組碼執行相關的也可以參考po主之前寫的JVM位元組碼執行模型及位元組碼指令集。
在Class檔案中,方法調用即是對常量池(ConstantPool)屬性工作表中的一個符號引用,在類載入的解析期或者運行時才能確定直接引用。 invokestatic 主要用於調用static關鍵字標記的靜態方法 invokespecial 主要用於調用私人方法,構造器,父類方法。 invokevirtual 虛方法,不確定調用那一個實作類別,比如Java中的重寫的方法調用。例子可以參考:從位元組碼指令看重寫在JVM中的實現 invokeinterface 介面方法,運行時才能確定實現介面的對象,也就是運行時確定方法的直接引用,而不是解析期間。 invokedynamic 這個作業碼的執行方法會關聯到一個動態調用點對象(Call Site object),這個call site 對象會指向一個具體的bootstrap 方法(方法的二進位位元組流資訊在BootstrapMethods屬性工作表中)的執行,invokedynamic指令的調用會有一個獨特的調用鏈,不像其他四個指令會直接調用方法,在實際的運行過程也相對前四個更加複雜。結合後面的例子,應該會比較直觀的理解這個指令。

關於方法調用的其他詳細的解釋可以參考官方文檔《The Java® Virtual Machine Specification Java8 Edition》-2.11.8 Method Invocation and Return Instructions。 lambda運算式運行機制

在看位元組碼細節之前,先來瞭解一下lambda運算式如何脫糖(desugar)。lambda的文法糖在編譯後的位元組流Class檔案中,會通過invokedynamic指令指向一個bootstrap方法(下文中部分會稱作“引導方法”),這個方法就是java.lang.invoke.LambdaMetafactory中的一個靜態方法。通過debug的方式,就可以看到該方法的執行,此方法源碼如下:

 public static CallSite metafactory(MethodHandles.Lookup caller,           String invokedName,           MethodType invokedType,           MethodType samMethodType,           MethodHandle implMethod,           MethodType instantiatedMethodType)            throws LambdaConversionException {        AbstractValidatingLambdaMetafactory mf;        mf = new InnerClassLambdaMetafactory(caller, invokedType,                                             invokedName, samMethodType,                                             implMethod, instantiatedMethodType,                                             false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);        mf.validateMetafactoryArgs();        return mf.buildCallSite();    }

在運行時期,虛擬機器會通過調用這個方法來返回一個CallSite(調用點)對象。簡述一下方法的執行過程,首先,初始化一個InnerClassLambdaMetafactory對象,這個對象的buildCallSite方法會將Lambda運算式先轉化成一個內部類,這個內部類是MethodHandles.Lookup caller的一個內部類,也即包含此Lambda運算式的類的內部類。這個內部類是通過位元組碼產生技術(jdk.internal.org.objectweb.asm)產生,再通過UNSAFE類載入到JVM。然後再返回綁定此內部類的CallSite對象,這個過程的源碼也可以看一下:

CallSite buildCallSite() throws LambdaConversionException {        // 通過位元組碼產生技術(jdk asm)產生代表lambda運算式體資訊的一個內部類的Class對象,因為是運行期產生,所以在編譯後的位元組碼資訊中並沒有這個內部類的位元組流資訊。        final Class<?> innerClass = spinInnerClass();        // incokedType即lambda運算式的調用方法類型,如下面樣本中的Consumer方法        if (invokedType.parameterCount() == 0) {            final Constructor<?>[] ctrs = AccessController.doPrivileged(                    new PrivilegedAction<Constructor<?>[]>() {                @Override                public Constructor<?>[] run() {                    Constructor<?>[] ctrs = innerClass.getDeclaredConstructors();                    if (ctrs.length == 1) {                        // 表示lambda運算式的內部類是私人的,所以需要擷取這個內部類的存取權限。                        ctrs[0].setAccessible(true);                    }                    return ctrs;                }                    });            if (ctrs.length != 1) {                throw new LambdaConversionException("Expected one lambda constructor for "                        + innerClass.getCanonicalName() + ", got " + ctrs.length);            }            try {            // 通過建構函式的newInstance方法,建立一個內部類對象                Object inst = ctrs[0].newInstance();                // MethodHandles.constant方法將這個內部類對象的資訊組裝並綁定到一個MethodHandle對象,作為ConstantCallSite的建構函式的參數“target”返回。後面對於Lambda運算式的調用,都會通過MethodHandle直接調用,不需再次產生CallSite.                return new ConstantCallSite(MethodHandles.constant(samBase, inst));            }            catch (ReflectiveOperationException e) {                throw new LambdaConversionException("Exception instantiating lambda object", e);            }        } else {            try {                UNSAFE.ensureClassInitialized(innerClass);                return new ConstantCallSite(                        MethodHandles.Lookup.IMPL_LOOKUP                             .findStatic(innerClass, NAME_FACTORY, invokedType));            }            catch (ReflectiveOperationException e) {                throw new LambdaConversionException("Exception finding constructor", e);            }        }    }

這個過程將產生一個代表lambda運算式資訊的內部類(也就是方法第一行的innerClass,這個類是一個 functional 類型介面的實作類別),這個內部類的Class位元組流是通過jdk asm 的ClassWriter,MethodVisitor,產生,然後再通過調用Constructor.newInstance方法產生這個內部類的對象,並將這個內部類對象綁定給一個MethodHandle對象,然後這個MethodHandle對象傳給CallSite對象(通過CallSite的建構函式賦值)。所以這樣就完成了一個將lambda運算式轉化成一個內部類對象,然後將內部類通過MethodHandle綁定到一個CallSite對象。CallSite對象就相當於lambda運算式的一個勾子。而invokedynamic指令就連結到這個CallSite對象來實現運行時綁定,也即invokedynamic指令在調用時,會通過這個勾子找到lambda所代表的一個functional介面對象(也即MethodHandle對象)。所以lambda的脫糖也就是在運行期通過bootstrap method的位元組碼資訊,轉化成一個MethodHandle的過程。
通過列印consumer對象的className(greeter.getClass().getName())可以得到結果是eight.Functionnal$$Lambda$1/659748578前面字元是Lambda運算式的ClassName,後面的659748578是剛才所述內部類的hashcode值。
下面通過具體的位元組碼指令詳細分析一下lambda的脫糖機制,並且看一下invokedynamic指令是怎麼給lambda在JVM中的實現帶來可能。如果前面所述過程還有不清晰,還可以參考下Oracle工程師在設計java8 Lambda運算式時候的一些思考:Translation of Lambda Expressions lambda運算式位元組碼指令樣本分析

先看一個簡單的樣本,樣本使用了java.util.function包下面的Consumer。
範例程式碼:(下面的Person對象只有一個String類型屬性:name,以及一個有參構造方法)

package eight;import java.util.function.Consumer;/** * Created by lijingyao on 15/11/2 19:13. */public class Functionnal {    public static void main(String[] args) {        Consumer<Person> greeter = (p) -> System.out.println("Hello, " + p.getName());        greeter.accept(new Person("Lambda"));    }}

用verbose命令看一下方法主體的位元組碼資訊,這裡暫時省略常量池資訊,後面會在符號引用到常量池資訊的地方具體展示。

public static void main(java.lang.String[]);    descriptor: ([Ljava/lang/String;)V    flags: ACC_PUBLIC, ACC_STATIC    Code:      stack=4, locals=2, args_size=1         0: invokedynamic #2,  0              // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;         5: astore_1         6: aload_1         7: new           #3                  // class eight/Person        10: dup        11: ldc           #4                  // String Lambda        13: invokespecial #5                  // Method eight/Person."<init>":(Ljava/lang/String;)V        16: invokeinterface #6,  2            // InterfaceMethod java/util/function/Consumer.accept:(Ljava/lang/Object;)V        21: return      LineNumberTable:        line 11: 0        line 12: 6        line 13: 21      LocalVariableTable:        Start  Length  Slot  Name   Signature            0      22     0  args   [Ljava/lang/String;            6      16     1 greeter   Ljava/util/function/Consumer;      LocalVariableTypeTable:        Start  Length  Slot  Name   Signature            6      16     1 greeter   Ljava/util/function/Consumer<Leight/Person;>;
invokedynamic指令特性

可以看到第一條指令就是代表了lambda運算式的實現指令,invokedynamic指令,這個指令是JSR-292開始應用的規範,而鑒於相容和擴充的考慮(可以參考Oracle工程師對於使用invokedynamic指令的原因),JSR-337通過這個指令來實現了lambda運算式。也就是說,只要有一個lambda運算式,就會對應一個invokedynamic指令。
先看一下第一行位元組碼指令資訊

0: invokedynamic #2, 0 0: 代表了在方法中這條位元組碼指令作業碼(Opcode)的位移索引。 invokedynamic就是該條指令的作業碼助記符。 #2, 0 是指令的運算元(Operand),這裡的#2表示運算元是一個對於Class常量池資訊的一個符號引用。逗號後面的0 是invokedynamic指令的預設值參數,到目前的JSR-337規範版本一直而且只能等於0。所以直接看一下常量池中#2的資訊。
invokedynamic在常量是有專屬的描述結構的(不像其他方法調用指令,關聯的是CONSTANT_MethodType_info結構)。
invokedynamic 在常量池中關聯一個CONSTANT_InvokeDynamic_info結構,這個結構可以明確invokedynamic指令的一個引導方法(bootstrap method),以及動態調用方法名和返回資訊。

常量池索引位置#2的資訊如下:

   #2 = InvokeDyn amic      #0:#44         // #0:accept:()Ljava/util/function/Consumer;   

結合CONSTANT_InvokeDynamic_info的結構資訊來看一下這個常量池表項包含的資訊。
CONSTANT_InvokeDynamic_info結構如下:

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

簡單解釋下這個CONSTANT_InvokeDynamic_info的結構: tag: 佔用一個位元組(u1)的tag,也即InvokeDynamic的一個標記值,其會轉化成一個位元組的tag值。可以看一下jvm spec中,常量池的tag值轉化表(這裡tag值對應=18):
bootstrap_method_attr_index:指向bootstrap_methods的一個有效索引值,其結構在屬性工作表的 bootstrap method 結構中,也描述在Class檔案的二進位位元組流資訊裡。下面是對應索引 0 的bootstrap method 屬性工作表的內容:

BootstrapMethods:      0: #40 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:          #41 (Ljava/lang/Object;)V         #42 invokestatic eight/Functionnal.lambda$main$0:(Leight/Person;)V         #43 (Leight/Person;)V      

這段位元組碼資訊展示了,引導方法就是LambdaMetafactory.metafactory方法。對照著前面LambdaMetafactory.metafactory的源碼一起閱讀。通過debug先看一下這個方法在運行時的參數值:

這個方法的前三個參數都是由JVM自動連結Call Site產生。方法最後返回一個CallSite對象,對應invokedynamic指令的運算元。
- name_and_type_index:代表常量池表資訊的一個有效索引值,其指向的常量池屬性工作表結構一定是一個CONSTANT_NameAndType_info屬性,代表了方法名稱和方法描述符資訊。再沿著 #44 索引看一下常量池相關項目的描述內容:

 #44 = NameAndType        #64:#65        // accept:()Ljava/util/function/Consumer; #64 = Utf8               accept #65 = Utf8               ()Ljava/util/function/Consumer;

通過以上幾項,可以很清楚得到invokedynamic的方法描述資訊。 其餘位元組碼指令解析

綜上,已經介紹了lombda運算式在位元組碼上的實現方式。其他指令,如果對位元組碼指令感興趣可以繼續閱讀,已經瞭解的可以略過,本小節和lambda本身沒有太大關聯。 第二條指令:5: astore_1 指令起始位移位置是5,主要取決於前面一個指令(invokedynamic)有兩個運算元,每個運算元佔兩個位元組(u2)空間,所以第二條指令就是從位元組位移位置5開始(後續的位移地址將不再解釋)。此指令執行後,當前方法的棧幀結構如下(註:此圖沒有畫出當前棧幀的動態連結以及返回地址的資料結構,圖中:左側局部變數表,右側運算元棧):

這裡為了畫圖方便,所以按照局部變數表和運算元棧的實際分配空間先畫出了幾個格子。因為位元組碼資訊中已經告知了[stack=4, locals=2, args_size=1]。也就是局部變數表的實際運行時空間最大佔用兩個Slot(一個Slot一個位元組,long,double類型變數需佔用兩個slot),運算元棧是4個slot,參數佔一個slot。這裡的args是main方法的String[] args參數。因為是個static方法,所以也沒有this變數的aload_0 指令。
2. 第三條: 6: aload_1將greeter 彈出局部變數表,壓入運算元棧。

3. 第四條:7: new #3初始化person對象指令,這裡並不等同於new關鍵字,new作業碼只是找到常量池的符號引用,執行到此行命令時,運行時堆區會建立一個有預設值的對象,如果是Object類型,那麼預設值是null,然後將這個對於預設值的引用地址壓入到運算元棧。其中#3 運算元指向的常量池Class屬性工作表的一個引用,可以看到這個常量池項為:#3 = Class #45 // eight/Person 。此時的運行時棧幀結構如下:

4. 第五條:10: dup 複製運算元棧棧頂的值,並且將該值入運算元棧棧頂。dup指令是一種對於初始化過程的編譯期最佳化。因前面的new作業碼並不會真正的建立對象,而是push一個引用到運算元棧,所以dup之後,這個棧頂的複製引用就可以用來給調用初始化方法(建構函式)的invokespecial提供運算元時消耗掉,同時原有的引用值就可以給其他比如對象引用的作業碼使用。此時棧幀結構如下圖:

5. 第六條:11: ldc #4 將運行時常量池的值入運算元棧,這裡的值是Lambda字串。#4 在常量池屬性工作表中結構資訊如下:

  #4 = String             #46            // Lambda  #46 = Utf8               Lambda

此時運行時棧幀結構如下:

6. 第七條:13: invokespecial #5 初始化Person對象的指令(#5指向了常量池Person的初始化方法eight/Person.””:(Ljava/lang/String;)V),也即調用Person建構函式的指令。此時”Lambda”常量池的引用以及 dup 複製的person引用地址出運算元棧。這條指令執行之後,才在堆中真正建立了一個Person對象。此時棧幀結構如下:
第八條:16: invokeinterface #6, 2 調用了Consumer的accept介面方法{greeter.accept(person)}。#6 逗號後面的參數2 是invokeinterface指令的參數,含義是介面方法的參數的個數加1,因為accpet方法只有一個參數,所以這裡是1+1=2。接著再看一下常量池項 #6 屬性工作表資訊:

   #6 = InterfaceMethodref #48.#49        // java/util/function/Consumer.accept:(Ljava/lang/Object;)V   #48 = Class              #67            // java/util/function/Consumer   #49 = NameAndType        #64:#62        // accept:(Ljava/lang/Object;)V   #67 = Utf8               java/util/function/Consumer   #62 = Utf8               (Ljava/lang/Object;)V   #64 = Utf8               accept

以上可以看出Consumer介面的泛型被擦除(編譯期間進行,所以位元組碼資訊中並不會包含泛型資訊),所以這裡並不知道實際的參數運算元類型。但是這裡可以得到實際對象的引用值,這裡accept方法執行,greeter和person引用出棧,如下圖:

8. 第九條:21: return 方法返回,因為是void方法,所以就是opcode就是return。此時運算元棧和局部變數表都是空,方法返回。最後再畫上一筆:
結語

本文只是通過Consumer介面分析lambda運算式的位元組碼指令,以及運行時的脫糖過程。也是把作業碼忘得差不多了,也順便再回顧一下。
從位元組碼看lambda可以追溯到源頭,所以也就能理解運行時的記憶體模型。
lambda運算式對應一個incokedynamic 指令,通過指令在常量池的符號引用,可以得到BootstrapMethods 屬性工作表對應的引導方法。在運行時,JVM會通過調用這個引導方法產生一個含有MethodHandle(CallSite的target屬性)對象的CallSite作為一個Lambda的回調點。Lambda的運算式資訊在JVM中通過位元組碼產生技術轉換成一個內部類,這個內部類被綁定到MethodHandle對象中。每次執行lambda的時候,都會找到運算式對應的回調點CallSite執行。一個CallSite可以被多次執行(在多次調用的時候)。如下面這種情況,只會有一個invokedynamic指令,在comparator調用comparator.compare或comparator.reversed方法時,都會通過CallSite找到其內部的MethodHandle,並通過MethodHandle調用Lambda的內部表示形式LambdaForm。

   public static void main(String[] args) {        Comparator<Person> comparator = (p1, p2) -> p1.getFirstName().compareTo(p2.getFirstName());        Person p1 = new Person("John", "Doe");        Person p2 = new Person("Alice", "Wonderland");        comparator.compare(p1, p2);            // > 0        comparator.reversed().compare(p1, p2);  // < 0    }

Lambda不僅用起來很方便,效能表現在多數情況也比匿名內部類好,效能方面可以參考一下Oracle的Sergey Kuksenko發布的 Lambda 效能報告。由上文可知,雖然在運行時需要轉化Lambda Form(見MethodHandle的form屬性產生過程),並且產生CallSite,但是隨著調用點被頻繁調用,通過JIT編譯最佳化等,效能會有明顯提升。並且,運行時脫糖也增強了編譯期的靈活性(其實在看位元組碼之前,一直以為Lambda可能是在編譯期脫糖成一個匿名內部類的Class,而不是通過提供一個boortrap方法,在運行時連結到調用點)。運行時產生調用點的方式實際的記憶體使用量率在多數情況也是低於匿名內部類(java8 之前版本的寫法)的方式。所以,在能使用lambda運算式的地方,我們盡量結合實際的效能測試情況,寫簡潔的運算式,盡量減少Lambda運算式內部捕獲變數(因為這樣會建立額外的變數對象),如果需要在運算式內部捕獲變數,可以考慮是否可以將變數寫成類的成員變數,也即盡量少給Lambda傳多餘的參數。希望本文能給Lambda的使用者一些參考。 資源

Translation of Lambda Expressions
JVM位元組碼執行模型及位元組碼指令集
The Java® Virtual Machine Specification Java8 Edition( JSR-337 )
invoke an interface method
Java 8 Lambdas - A Peek Under the Hood
JDK 8: Lambda Performance study

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.