1. 程式人生 > >通過位元組碼分析 JDK8 中 Lambda 表示式編譯及執行機制

通過位元組碼分析 JDK8 中 Lambda 表示式編譯及執行機制

關於Lambda位元組碼相關的文章,很早之前就想寫了,[蜂潮運動]APP 產品的後端技術,能快速迭代,除了得益於整體微服架構之外,語言層面上,也是通過Java8的lambda表示式的運用以及rxJava響應式程式設計框架,使程式碼更加簡潔易維護,呼叫方式更加便捷。

本文將介紹JVM中的方法呼叫相關的位元組碼指令,重點解析JDK7(JSR-292)之後新增的invokedynamic指令給lambda表示式的動態呼叫特性提供的實現機制,最後再探討一下lambda效能方面的話題。

方法呼叫的位元組碼指令

在介紹invokedynamic指令前,先回顧一下JVM規範中的所有方法呼叫的位元組碼指令。其他關於位元組碼執行相關的也可以參考po主之前寫的

JVM位元組碼執行模型及位元組碼指令集

在Class檔案中,方法呼叫即是對常量池(ConstantPool)屬性表中的一個符號引用,在類載入的解析期或者執行時才能確定直接引用。

  1. invokestatic 主要用於呼叫static關鍵字標記的靜態方法
  2. invokespecial 主要用於呼叫私有方法,構造器,父類方法。
  3. invokevirtual 虛方法,不確定呼叫那一個實現類,比如Java中的重寫的方法呼叫。例子可以參考:從位元組碼指令看重寫在JVM中的實現
  4. invokeinterface 介面方法,執行時才能確定實現介面的物件,也就是執行時確定方法的直接引用,而不是解析期間。
  5. invokedynamic 這個操作碼的執行方法會關聯到一個動態呼叫點物件(Call Site object),這個call site 物件會指向一個具體的bootstrap 方法(方法的二進位制位元組流資訊在BootstrapMethods屬性表中)的執行,invokedynamic指令的呼叫會有一個獨特的呼叫鏈,不像其他四個指令會直接呼叫方法,在實際的執行過程也相對前四個更加複雜。結合後面的例子,應該會比較直觀的理解這個指令。

lambda表示式執行機制

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

Java
123456789101112131415 publicstaticCallSite metafactory(MethodHandles.Lookup caller,StringinvokedName,MethodType invokedType,MethodType samMethodType,MethodHandle implMethod,MethodType instantiatedMethodType)throwsLambdaConversionException{AbstractValidatingLambdaMetafactory mf;mf=newInnerClassLambdaMetafactory(caller,invokedType,invokedName,samMethodType,implMethod,instantiatedMethodType,false,EMPTY_CLASS_ARRAY,EMPTY_MT_ARRAY);mf.validateMetafactoryArgs();returnmf.buildCallSite();}

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

Java
12345678910111213141516171819202122232425262728293031323334353637383940414243 CallSite buildCallSite()throwsLambdaConversionException{// 通過位元組碼生成技術(jdk asm)生成代表lambda表示式體資訊的一個內部類的Class物件,因為是執行期生成,所以在編譯後的位元組碼資訊中並沒有這個內部類的位元組流資訊。finalClass>innerClass=spinInnerClass();// incokedType即lambda表示式的呼叫方法型別,如下面示例中的Consumer方法if(invokedType.parameterCount()==0){finalConstructor>[]ctrs=AccessController.doPrivileged(newPrivilegedAction[]>(){@OverridepublicConstructor>[]run(){Constructor>[]ctrs=innerClass.getDeclaredConstructors();if(ctrs.length==1){// 表示lambda表示式的內部類是私有的,所以需要獲取這個內部類的訪問許可權。ctrs[0].setAccessible(true);}returnctrs;}});if(ctrs.length!=1){thrownewLambdaConversionException("Expected one lambda constructor for "+innerClass.getCanonicalName()+", got "+ctrs.length);}try{// 通過建構函式的newInstance方法,建立一個內部類物件Objectinst=ctrs[0].newInstance();// MethodHandles.constant方法將這個內部類物件的資訊組裝並繫結到一個MethodHandle物件,作為ConstantCallSite的建構函式的引數“target”返回。後面對於Lambda表示式的呼叫,都會通過MethodHandle直接呼叫,不需再次生成CallSite.returnnewConstantCallSite(MethodHandles.constant(samBase,inst));}catch(ReflectiveOperationExceptione){thrownewLambdaConversionException("Exception instantiating lambda object",e);}}else{try{UNSAFE.ensureClassInitialized(innerClass);returnnewConstantCallSite(MethodHandles.Lookup.IMPL_LOOKUP.findStatic(innerClass,NAME_FACTORY,invokedType));}catch(ReflectiveOperationExceptione){thrownewLambdaConversionException("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,以及一個有參構造方法)

Java
1234567891011121314 packageeight;importjava.util.function.Consumer;/** * Created by lijingyao on 15/11/2 19:13. */publicclassFunctionnal{publicstaticvoidmain(String[]args){Consumer greeter=(p)->System.out.println("Hello, "+p.getName());greeter.accept(newPerson("Lambda"));}}

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

Java
12345678910111213141516171819202122232425 publicstaticvoidmain(java.lang.String[]);descriptor:([Ljava/lang/String;)Vflags:ACC_PUBLIC,ACC_STATICCode:stack=4,locals=2,args_size=10:invokedynamic#2,0// InvokeDynamic #0:accept:()Ljava/util/function/Consumer;5:astore_16:aload_17:new#3// class eight/Person10:dup11:ldc#4// String Lambda13:invokespecial#5// Method eight/Person."":(Ljava/lang/String;)V16:invokeinterface#6,2// InterfaceMethod java/util/function/Consumer.accept:(Ljava/lang/Object;)V21:returnLineNumberTable:line11:0line12:6line13:21LocalVariableTable:Start  Length  Slot  Name   Signature0220args[Ljava/lang/String;6161greeter   Ljava/util/function/Consumer;LocalVariableTypeTable:Start  Length  Slot  Name   Signature6161greeter   Ljava/util/function/Consumer;

invokedynamic指令特性

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

0: invokedynamic #2, 0

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

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

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

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

Java
12345 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):
     Constant pool tags
  • bootstrap_method_attr_index:指向bootstrap_methods的一個有效索引值,其結構在屬性表的 bootstrap method 結構中,也描述在Class檔案的二進位制位元組流資訊裡。下面是對應索引 0 的bootstrap method 屬性表的內容:
Java
123456 BootstrapMethods:0:#40invokestatic 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#42invokestatic eight/Functionnal.lambda$main$0:(Leight/Person;)V#43(Leight/Person;)V

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

LambdaMetafactory.metafactory arguments