当前位置: 新豪天地登录网址 > www.3559.com > 正文

和invokedynamic指令详细分析,通过字节码分析JDK

时间:2019-10-08 08:23来源:www.3559.com
通过以上几项,可以很清楚得到invokedynamic的方法描述信息。 1.虚拟机解析 hotspot对 invokedynamic 指令的解释如下: CASE(_invokedynamic): { u4 index = Bytes::get_native_u4; ConstantPoolCacheEntry* cache = cp-

通过以上几项,可以很清楚得到invokedynamic的方法描述信息。

1.虚拟机解析

hotspot对invokedynamic指令的解释如下:

      CASE(_invokedynamic): {        u4 index = Bytes::get_native_u4;        ConstantPoolCacheEntry* cache = cp->constant_pool()->invokedynamic_cp_cache_entry_at;        // We are resolved if the resolved_references field contains a non-null object (CallSite, etc.)        // This kind of CP cache entry does not need to match the flags byte, because        // there is a 1-1 relation between bytecode type and CP entry type.        if (! cache->is_resolved((Bytecodes::Code) opcode)) {          CALL_VM(InterpreterRuntime::resolve_from_cache(THREAD, (Bytecodes::Code)opcode),                  handle_exception);          cache = cp->constant_pool()->invokedynamic_cp_cache_entry_at;        }        Method* method = cache->f1_as_method();        if (VerifyOops) method->verify();        if (cache->has_appendix {          ConstantPool* constants = METHOD->constants();          SET_STACK_OBJECT(cache->appendix_if_resolved(constants), 0);          MORE_STACK;        }        istate->set_msg(call_method);        istate->set_callee;        istate->set_callee_entry_point(method->from_interpreted_entry;        istate->set_bcp_advance;        // Invokedynamic has got a call counter, just like an invokestatic -> increment!        BI_PROFILE_UPDATE_CALL();        UPDATE_PC_AND_RETURN; // I'll be back...      }

使用invokedynamic_cp_cache_entry_at获取常量池对象,然后检查是否已经解析过,如果没有就解析反之复用,然后设置方法字节码,留待后面解释执行。那么,重点是这个解析。我们对照着jvm spec来看。

根据jvm文档的描述,invokedynamic的操作数指向常量池一个动态调用点描述符(dynamic call site specifier)。
动态调用点描述符是一个CONSTANT_InvokeDynamic_info结构体:

CONSTANT_InvokeDynamic_info { u1 tag; u2 bootstrap_method_attr_index; u2 name_and_type_index;}
  • tag 表示这个结构体的常量,不用管
  • bootstrap_method_attr_index 启动方法数组
  • name_and_type_index 一个名字 类型的描述字段,就像这样Object p放到虚拟机里面表示是Ljava/lang/Object; p

然后启动方法数组结构是这样:

BootstrapMethods_attribute { ... u2 num_bootstrap_methods; {     u2 bootstrap_method_ref;    u2 num_bootstrap_arguments;    u2 bootstrap_arguments[num_boot]    } bootstrap_methods[num_bootstrap_methods];}

就是一个数组,每个元素是{指向MethodHandle的索引,启动方法参数个数,启动方法参数}

MethodlHandle是个非常重要的结构,指导了虚拟机对于这个启动方法的解析,先关注一下这个结构:

CONSTANT_MethodHandle_info { u1 tag;//表示该结构体的常量tag,可以忽略 u1 reference_kind; u2 reference_index;}
  • reference_kind是[1,9]的数,它表示这个method handle的类型,这个字段和字节码的行为有关。
  • reference_index 根据reference_kind会指向常量池的不同类型,具体来说
    • reference_kind==1,3,4 指向CONSTANT_Fieldref_info结构,表示一个类的字段
    • reference_kind==5,8,指向CONSTANT_Methodref_info,表示一个类的方法
    • reference_kind==6,7, 同上,只是兼具接口的方法或者类的方法的可能。
    • reference_kind==9,指向CONSTATN_InterfaceMethodref_info,表示一个接口方法

通过invokedynamic,我们可以得

  1. 名字 描述符的表示(由name_and_type_index给出)
  2. 一个启动方法数组(由bootstrap_method_attr_index给出)

就拿这个captureValue方法里面的表达式来说,在这里会生成invokedynamic 指令:

Groovy

接下来看一下 groovy 是如何使用 inDy 指令的。先复习一遍 groovy 的方法派发。

每当 Groovy 调用一个方法时,它不会直接调用它,而是要求一个中间层来代替它。 中间层通过钩子方法允许我们更改方法调用的行为。这个中间层就是 MOP(meta object proctol),MOP 主要承载的类就是 MetaClass 。一个简化版的 MOP 主要有这些方法:

  • invokeMethod(String methodName, Object args)
  • methodMissing(String name, Object arguments)
  • getProperty(String propertyName)
  • setProperty(String propertyName, Object newValue)
  • propertyMissing(String name)

可以大致认为在 Groovy 中的每个方法和属性访问调用都会转化上面的方法调用。而这些方法可以在运行时通过重写修改它的默认行为,MOP 作为方法派发的中心枢纽为 Groovy 提供了非常灵活的动态编程的能力。

现在来看一下一段简短的 groovy 代码,

class Test{ int a = 0; static void main{ Test wtf = new Test() wtf.a wtf.doSomething() }}

通过 groovyc -indy Test.groovy 把它编译成字节码。 indy 选项的意思就是启用 invokedynamic 支持。

看一下编译后的 main 方法。

public static void main(java.lang.String...); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS Code: stack=1, locals=2, args_size=1 0: ldc #2 // class Test 2: invokedynamic #44, 0 // InvokeDynamic #0:init:(Ljava/lang/Class;)Ljava/lang/Object; 7: invokedynamic #50, 0 // InvokeDynamic #1:cast:(Ljava/lang/Object;)LTest; 12: astore_1 13: aload_1 14: pop 15: aload_1 16: invokedynamic #56, 0 // InvokeDynamic #2:getProperty:Ljava/lang/Object; 21: pop 22: aload_1 23: invokedynamic #61, 0 // InvokeDynamic #3:invoke:Ljava/lang/Object; 28: pop 29: return

可以看到一共有 4 条 inDy 指令,包括构造函数,访问成员变量,和不存在的方法调用都是 通过 invokedynamic 实现的。

再看一下引导方法表

BootstrapMethods: 0: #38 invokestatic org/codehaus/groovy/vmplugin/v7/IndyInterface.bootstrap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;I)Ljava/lang/invoke/CallSite; Method arguments: #39 <init> #40 0 1: #38 invokestatic org/codehaus/groovy/vmplugin/v7/IndyInterface.bootstrap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;I)Ljava/lang/invoke/CallSite; Method arguments: #46 () #40 0 2: #38 invokestatic org/codehaus/groovy/vmplugin/v7/IndyInterface.bootstrap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;I)Ljava/lang/invoke/CallSite; Method arguments: #51 a #52 4 3: #38 invokestatic org/codehaus/groovy/vmplugin/v7/IndyInterface.bootstrap:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;I)Ljava/lang/invoke/CallSite; Method arguments: #58 doSomething #40 0

可以发现所有 inDy 指令的引导方法都是 IndyInterface.bootstrap

以方法调用的 inDy 指令为例,它的方法名称是 "invoke",方法签名是 methodType(Object.class,Test.class),BSM 方法还附带两个参数分别是实际的方法名:"doSomething" 和一个标志:0

BSM 方法最终调用的是 realBootstrap 方法:

private static CallSite realBootstrap(Lookup caller, String name, int callID, MethodType type, boolean safe, boolean thisCall, boolean spreadCall) { MutableCallSite mc = new MutableCallSite; //这里是 MutableCallSite,lambda 表达式用的是 ConstantCallSite MethodHandle mh = makeFallBack(mc,caller.lookupClass(),name,callID,type,safe,thisCall,spreadCall); mc.setTarget; return mc;}

主要的代码是调用 makeFallBack 来获取一个临时的 MethodHandle。因为在第一步 groovy 无法确定接收者,也是就是 invoke 方法的第一个实参,必须要在第二步确定 CallSite 后才会传递过来。所以方法解析要放在第二步。

protected static MethodHandle makeFallBack(MutableCallSite mc, Class<?> sender, String name, int callID, MethodType type, boolean safeNavigation, boolean thisCall, boolean spreadCall) { MethodHandle mh = MethodHandles.insertArguments(SELECT_METHOD, 0, mc, sender, name, callID, safeNavigation, thisCall, spreadCall, /*dummy receiver:*/ 1); //MethodHandle(Object.class,Object[].class) mh = mh.asCollector(Object[].class, type.parameterCount. asType; return mh;}

这个 fallback 方法其实就是 selectMethodinsertArguments 在这里主要做了一个柯里化的操作,因为selectMethod 的方法签名是

methodType(Object.class, MutableCallSite.class, Class.class, String.class, int.class, Boolean.class, Boolean.class, Boolean.class, Object.class, Object[].class)

而 inDy 要求的方法签名却是

methodType(Object.class,Test.class)。

www.3559.com ,所以得经过 insertArguments 的变换,把确定的值填充进去,用最后的数组参数来接收 inDy 传递的参数。这样这个方法就能够被 inDy 调用了。第一步创建 CallSite 到这里就结束。

第二步,就是 selectMethod 方法的调用,这时候 groovy 已经知道方法的接收者 arguments[0]

public static Object selectMethod(MutableCallSite callSite, Class sender, String methodName, int callID, Boolean safeNavigation, Boolean thisCall, Boolean spreadCall, Object dummyReceiver, Object[] arguments) throws Throwable { Selector selector = Selector.getSelector(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, arguments); selector.setCallSiteTarget(); MethodHandle call = selector.handle.asSpreader(Object[].class, arguments.length); call = call.asType(MethodType.methodType(Object.class,Object[].class)); return call.invokeExact(arguments);}

首先创建一个方法解析器,在这里是 MethodSelector。接着调用 setCallSiteTarget(),这个方法就是用来解析实际的方法。具体的过程还是很复杂的,所以也没法说清楚,大体来说就是确定接收者的 MetaClass,决定这个方法是实际的方法,还是交给 MetaClass 的钩子方法,然后就是创建这个方法的 MethodHandle,然后把这个 MethodHandle 的签名转化为要求的签名。这时 selecor.handle 就是最终调用的方法句柄了。接下来就是最终的方法调用了,到这里 inDy 指令就执行完毕了。

还有一个方法值得留意:

public void doCallSiteTargetSet() { if  { if (LOG_ENABLED) LOG.info("call site stays uncached"); } else { callSite.setTarget; if (LOG_ENABLED) LOG.info("call site target set, preparing outside invocation"); }}

这也是为什么用 MutableCallSite 的原因,如果编译器认为这个方法是可以缓存,那么就会把这个 CallSite 绑定到实际的 MethodHandle,后续的调用就不用再重新解析了。

下面通过具体的字节码指令详细分析一下lambda的脱糖机制,并且看一下invokedynamic指令是怎么给lambda在JVM中的实现带来可能。如果前面所述过程还有不清晰,还可以参考下Oracle工程师在设计java8 Lambda表达式时候的一些思考:Translation of Lambda Expressions

方法引用(Method reference)和invokedynamic指令详细分析

invokedynamic是jvm指令集里面最复杂的一条。本文将详细分析invokedynamic指令是如何实现方法引用(Method reference)的。

具体言之,有这样一个方法引用:

interface Encode {    void encode(Derive person);}class Base {    public void encrypt() {        System.out.println("Base::speak");    }}class Derive extends Base {    @Override    public void encrypt() {        System.out.println("Derive::speak");    }}public class MethodReference {    public static void main(String[] args) {        Encode encode = Base::encrypt;        System.out.println;    }}

使用javap -verbose MethodReference.class查看对应字节码:

// 常量池Constant pool:   #1 = Methodref          #6.#22         // java/lang/Object."<init>":()V   #2 = InvokeDynamic      #0:#27         // #0:encode:()LEncode;   #3 = Fieldref           #28.#29        // java/lang/System.out:Ljava/io/PrintStream;   #4 = Methodref          #30.#31        // java/io/PrintStream.println:(Ljava/lang/Object;)V   #5 = Class              #32            // MethodReference   #6 = Class              #33            // java/lang/Object   #7 = Utf8               <init>   #8 = Utf8               ()V   #9 = Utf8               Code  #10 = Utf8               LineNumberTable  #11 = Utf8               LocalVariableTable  #12 = Utf8               this  #13 = Utf8               LMethodReference;  #14 = Utf8               main  #15 = Utf8               ([Ljava/lang/String;)V  #16 = Utf8               args  #17 = Utf8               [Ljava/lang/String;  #18 = Utf8               encode  #19 = Utf8               LEncode;  #20 = Utf8               SourceFile  #21 = Utf8               MethodReference.java  #22 = NameAndType        #7:#8          // "<init>":()V  #23 = Utf8               BootstrapMethods  #24 = MethodHandle       #6:#34         // 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;  #25 = MethodType         #35            //  V  #26 = MethodHandle       #5:#36         // invokevirtual Base.encrypt:()V  #27 = NameAndType        #18:#37        // encode:()LEncode;  #28 = Class              #38            // java/lang/System  #29 = NameAndType        #39:#40        // out:Ljava/io/PrintStream;  #30 = Class              #41            // java/io/PrintStream  #31 = NameAndType        #42:#43        // println:(Ljava/lang/Object;)V  #32 = Utf8               MethodReference  #33 = Utf8               java/lang/Object  #34 = Methodref          #44.#45        // 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;  #35 = Utf8               V  #36 = Methodref          #46.#47        // Base.encrypt:()V  #37 = Utf8               ()LEncode;  #38 = Utf8               java/lang/System  #39 = Utf8               out  #40 = Utf8               Ljava/io/PrintStream;  #41 = Utf8               java/io/PrintStream  #42 = Utf8               println  #43 = Utf8               (Ljava/lang/Object;)V  #44 = Class              #48            // java/lang/invoke/LambdaMetafactory  #45 = NameAndType        #49:#53        // 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;  #46 = Class              #54            // Base  #47 = NameAndType        #55:#8         // encrypt:()V  #48 = Utf8               java/lang/invoke/LambdaMetafactory  #49 = Utf8               metafactory// 字节码指令 public static void main(java.lang.String[]);     0: invokedynamic #2,  0              // InvokeDynamic #0:encode:()LEncode;     5: astore_1     6: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;     9: aload_1    10: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/Object;)V    13: return// 属性SourceFile: "MethodReference.java"InnerClasses:     public static final #51= #50 of #56; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandlesBootstrapMethods:  0: #24 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:      #25 V      #26 invokevirtual Base.encrypt:()V      #25 V

使用invokedynamic指令生成encode对象,然后存入局部变量槽#1。接着获取getstatic获取java/lang/System类的out字段,最后局部变量槽#1作为参数压栈,invokevirtual虚函数调用System.outprintln方法。

那么invokedynamic到底是怎么生成encode对象的呢?

接着,就进入运行期了。VM首先将上述参数压栈,调用bootstrap method,简单看一下此方法里做了什么:

  • Invokedynamic - Java’s Secret Weapon
  • 和译文 Invokedynamic:Java的秘密武器 - 知乎专栏

简单解释下这个CONSTANT_InvokeDynamic_info的结构:

总结

到这里就结束了整个流程,文章有点长,总结一下:

  1. 虚拟机遇到invokedynamic,开始解析操作数
  2. 根据invokedynamic #0:#27获取到启动方法和一个名字 描述符
    其中启动方法是
BootstrapMethods:  0: #24 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:      #25 V      #26 invokevirtual Base.encrypt:()V      #25 V

名字 描述符

 #27 = NameAndType        #18:#37        // encode:()LEncode;
  1. 启动方法指向LambdaMetafactory.metafactory,但是不会直接调用而是通过MethdHandle间接调用。调用位置位于CallSite.makeCallSite()
  2. LambdaMetafactory.metafactory()其实使用InnerClassLambdaMetafactory.buildCallSite()创建了最后的CallSite
  3. buildCallSite()会创建一个.class,
  4. buildCallSite()会向最后的CallSite里面放入一个可调用的MethdHandle
  5. 这个MethodHandle指向的是一个总是返回刚刚创建的.class类的实例的方法,由MethodHandles.constant(samBase, inst)完成
  6. 最后,用invokevirtual调用CallSite里面的MethdHandle,返回.class类的示例,即inst,即new MethodReference$$Lambda$1
      …………
      if (invokedType.parameterCount() == 0) {
            final Constructor<?>[] ctrs = AccessController.doPrivileged(
                    new PrivilegedAction<Constructor<?>[]>() {
                @Override
                public Constructor<?>[] run() {
                    Constructor<?>[] ctrs = innerClass.getDeclaredConstructors();
                    if (ctrs.length == 1) {
                        // The lambda implementing inner class constructor is private, set
                        // it accessible (by us) before creating the constant sole instance
                        ctrs[0].setAccessible(true);
                    }
                    return ctrs;
                }
                    });
            if (ctrs.length != 1) {
                throw new LambdaConversionException("Expected one lambda constructor for "
                          innerClass.getCanonicalName()   ", got "   ctrs.length);
            }

            try {
                Object inst = ctrs[0].newInstance();
                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);
            }
        }

java8 lambda 表达式

lambda 表达式 是怎么使用 inDy 呢?以一段简单的代码为例

public class LambdaTest { public static void main(String[] args) { Runnable r = () -> System.out.println(Arrays.toString; r.run(); }}

javap -v -p LambdaTest 查看字节码,可以发现寥寥几行 java 代码生成的字节码却不少,单单常量池常量就有 66 个之多。输出见 LambdaTest.class。

可以发现多出了一个新方法,方法体就是 lambda 体(lambda body),转换为源码如下:

private static void lambda$main$0(java.lang.String[] args){ System.out.println(Arrays.toString;}

主要看一下 main 方法,并没有直接调用上面的方法,而是出现一条 inDy 指令:

public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=2, args_size=1 0: aload_0 1: invokedynamic #2, 0 // InvokeDynamic #0:run:([Ljava/lang/String;)Ljava/lang/Runnable; 6: astore_1 7: aload_1 8: invokeinterface #3, 1 // InterfaceMethod java/lang/Runnable.run:()V 13: return

可以看到 inDy 指向一个类型为 CONSTANT_InvokeDynamic_info 的常量项 #2,另外 0 是预留参数,暂时没有作用。

#2 = InvokeDynamic #0:#30 // #0:run:([Ljava/lang/String;)Ljava/lang/Runnable;

#0 表示在 Bootstrap methods 表中的索引:

BootstrapMethods: 0: #27 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: #28 ()V #29 invokestatic com/company/LambdaTest.lambda$main$0:([Ljava/lang/String;)V #28 ()V

#30 则是一个 CONSTANT_NameAndType_info,表示方法名和方法类型,这个会作为参数传递给 BSM。

#30 = NameAndType #43:#44 // run:([Ljava/lang/String;)Ljava/lang/Runnable;

再看回表中的第 0 项,#27 是一个 CONSTANT_MethodHandle_info,实际上是个 MethodHandle对象,这个句柄指向的就是 BSM 方法。在这里就是:

java.lang.invoke.LambdaMetafactory.metafactory(MethodHandles.Lookup,String,MethodType,MethodType,MethodHandle,MethodType)

BSM 前三个参数是固定的,后面还可以附加任意数量的参数,但是参数的类型是有限制的,参数类型只能是

  • String
  • Class
  • int
  • long
  • float
  • double
  • MethodHandle
  • MethodType

LambdaMetafactory.metafactory 带多三个参数,这些的参数的值由 Bootstrap methods 表 提供:

Method arguments: #25 ()V #26 invokestatic com/company/LambdaTest.lambda$main$0:V

inDy 所需要的数据大概就是这些,可参考 Java8学习笔记 -- InvokeDynamic指令

  • CSDN博客

每一个 inDy 指令都称为 Dynamic Call Site,根据 jvm 规范所说的,inDy 可以分为两步,这两步部分代码代码是在 java 层的,给 metafactory 方法设断点可以看到一些行为。

第一步 inDy 需要一个 CallSite,CallSite 是由 BSM 返回的,所以这一步就是调用 BSM 方法。代码可参考:java.lang.invoke.CallSite#makeSite

调用 BSM 方法可以看作 invokevirtual 指令执行一个 invoke 方法,方法签名如下:

invoke:(MethodHandle,Lookup,String,MethodType,/*其他附加静态参数*/)CallSite

前四个参数是固定的,被依次压入操作栈里

  1. MethodHandle,实际上这个方法句柄就是指向 BSM
  2. Lookup, 也就是调用者,是 Indy 指令所在的类的上下文,可以通过 Lookup#lookupClass()获取这个类
  3. name ,lambda 所实现的方法名,也就是"run"
  4. invokedType,调用点的方法签名,这里是 methodType(Runnable.class,String[].class)

接下来就是附加参数,这些参数是灵活的,由Bootstrap methods 表提供,这里分别是:

  1. samMethodType,其实就是 Runnable.run 的描述符: methodType(void.class)。sam 就 single public abstract method 的缩写
  2. implMethod: 编译器给生成的 desugar 方法,是一个 MethodHandle:caller.findStatic(LambdaTest.class,"lambda$main$0",methodType(void.class))
  3. instantiatedMethodType: Runnable.run 运行时的描述符,如果方法泛型的,那这个类型可能不一样。这里是 methodType(void.class)

上面说的固定其实应该是指 inDy 传递的实参类型是固定的,BSM 形参声明可以是随意,保证 BSM 能被调用就行,比如说 Lookup 声明为 Object 不影响调用。

接下来就是执行 LambdaMetafactory.metafactory 方法了,它会创建一个匿名类,这个类是通过 ASM 编织字节码在内存中生成的,然后直接通过 unsafe 直接加载而不会写到文件里。不过可以通过下面的虚拟机参数让它运行的时候输出到文件

-Djdk.internal.lambda.dumpProxyClasses=<path>

这个类是根据 lambda 的特点生成的,输出后可以看到,在这个例子中是这样的:

import java.lang.invoke.LambdaForm.Hidden;// $FF: synthetic classfinal class LambdaTest$$Lambda$1 implements Runnable { private final String[] arg$1; private LambdaTest$$Lambda$1(String[] var1) { this.arg$1 = var1; } private static Runnable get$Lambda(String[] var0) { return new LambdaTest$$Lambda$1; } @Hidden public void run() { LambdaTest.lambda$main$0(this.arg$1); }}

然后就是创建一个 CallSite,绑定一个 MethodHandle,指向的方法其实就是生成的类中的静态方法 LambdaTest$$Lambda$1.get$LambdaRunnable。然后把调用点对象返回,到这里 BSM 方法执行完毕。

更详细的可参考:

  • 浅谈Lambda Expression - 简书
  • [Java] 关于OpenJDK对Java 8 lambda表达式的运行时实现的查看方式 - 知乎专栏

第二步,就是执行这个方法句柄了,这个过程就像 invokevirtual 指令执行 MethodHandle#invokeExact 一样,

加上 inDy 上面那一条 aload_0 指令,这是操作数栈有两个分别是:

  1. args[],lambda 里面调用了 main 方法的参数
  2. 调用点对象,实际上是方法句柄。如果是 CostantCallSite 的时候,inDy 会直接跟他的方法句柄链接。见代码:MethodHandleNatives.java#L255

传入 args,执行方法,返回一个 Runnable 对象,压入栈顶。到这里 inDy 就执行完毕。

接下来的指令就很好理解,astore_1 把栈顶的 Runnable 对象放到局部变量表的槽位1,也是变量 r。剩下的就是再拿出来调用 run 方法。

此时运行时栈帧结构如下:

2.手动解析

可以手动模拟一下解析,看看最后得到的数据是什么样的。在这个例子中:

  0: invokedynamic #2,  0   //第二个operand总是0

查看常量池#2项:

#2 = InvokeDynamic      #0:#27         // #0:encode:()LEncode;#27 = NameAndType        #18:#37        // encode:()LEncode;BootstrapMethods:  0: #24 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:      #25 V      #26 invokevirtual Base.encrypt:()V      #25 V

得到的名字 描述符是:Encode.encode(),启动方法数组有一个元素,回忆下之前说的,这个元素构成如下:

{指向MethodHandle的索引,启动方法参数个数,启动方法参数}

这里得到的MethodHandle表示的是LambdaMetafactory.metafactory:

#24 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;`

启动方法参数有:

  • #25 V
  • #26 invokevirtual Base.encrypt:()V
  • #25 V

编译之后就会在同类生成对应的instance-capturing方法:

inDy(invokedynamic)是 java 7 引入的一条新的虚拟机指令,这是自 1.0 以来第一次引入新的虚拟机指令。到了 java 8 这条指令才第一次在 java 应用,用在 lambda 表达式中。 indy 与其他 invoke 指令不同的是它允许由应用级的代码来决定方法解析。所谓应用级的代码其实是一个方法,在这里这个方法被称为引导方法(Bootstrap Method),简称 BSM。BSM 返回一个 CallSite 对象,这个对象就和 inDy 链接在一起了。以后再执行这条 inDy 指令都不会创建新的 CallSite 对象。CallSite 就是一个 MethodHandle的 holder。方法句柄指向一个调用点真正执行的方法。

这里为了画图方便,所以按照局部变量表和操作数栈的实际分配空间先画出了几个格子。因为字节码信息中已经告知了[stack=4, locals=2, args_size=1]。也就是局部变量表的实际运行时空间最大占用两个Slot(一个Slot一个字节,long,double类型变量需占用两个slot),操作数栈是4个slot,参数占一个slot。这里的args是main方法的String[] args参数。因为是个static方法,所以也没有this变量的aload_0 指令。

3.1 LambdaMetafactory.metafactory()调用前

要知道参数是什么意思,可以从它的调用者来管中窥豹:

 static CallSite makeSite(MethodHandle bootstrapMethod,                             // Callee information:                             String name, MethodType type,                             // Extra arguments for BSM, if any:                             Object info,                             // Caller information:                             Class<?> callerClass) {        MethodHandles.Lookup caller = IMPL_LOOKUP.in(callerClass);        CallSite site;        try {            Object binding;            info = maybeReBox;            if (info == null) {                binding = bootstrapMethod.invoke(caller, name, type);            } else if (!info.getClass().isArray {                binding = bootstrapMethod.invoke(caller, name, type, info);            } else {                Object[] argv =  info;                maybeReBoxElements;                switch (argv.length) {                ...                case 3:                    binding = bootstrapMethod.invoke(caller, name, type,                                                     argv[0], argv[1], argv[2]);                    break;                ...                }            }            //System.out.println("BSM for " name type " => " binding);            if (binding instanceof CallSite) {                site =  binding;            }  else {                throw new ClassCastException("bootstrap method failed to produce a CallSite");            }            ...        } catch (Throwable ex) {            ...        }        return site;    }

java.lang.invoke.LambdaMetafactory的调用是通过MethodHandle引发的,所以可能还需要补一下MethodHandle的用法,百度一搜一大堆,javadoc也给出了使用示例:

String s;MethodType mt; MethodHandle mh;MethodHandles.Lookup lookup = MethodHandles.lookup();// mt is (char,char)Stringmt = MethodType.methodType(String.class, char.class, char.class);mh = lookup.findVirtual(String.class, "replace", mt);s =  mh.invoke("daddy",'d','n');// invokeExact(Ljava/lang/String;CC)Ljava/lang/String;assertEquals(s, "nanny");

回到源码,关键是这句:

binding = bootstrapMethod.invoke(caller, name, type,                               argv[0], argv[1], argv[2]);

argv[0],argv[1],argv[2]分别表示之前启动方法的三个参数
caller即调用者,这里是MethodReference这个类,然后name和type参见下面的详细解释:

  • MethodHandles.Lookup caller 表示哪个类引发了调动
  • String invokedName 表示生成的类的方法名,对应例子的encode
  • MethodType invokedType 表示CallSite的函数签名,其中参数类型表示捕获变量的类型,返回类型是类要实现的接口的名字,对应例子的()Encode,即要生成一个类,这个类没有捕获自由变量,然后这个类要实现Encode接口(返回类型为生成的类要实现的接口)
    接下来
  • MethodType samMethodType 表示要实现的方法的函数签名和返回值,对于例子的#25 V,即实现方法带有一个形参,返回void
  • MethodHandle implMethod 表示实现的方法里面应该调用的函数,对于例子的#26 invokevirtual Base.encrypt:()V,表示调用Base的虚函数encrypt,返回void
  • MethodType instantiatedMethodType 表示调用方法的运行时描述符,如果不是泛型就和samMethodType一样
    private Class<?> spinInnerClass() throws LambdaConversionException {
        String[] interfaces;
        …………
        cw.visit(CLASSFILE_VERSION, ACC_SUPER   ACC_FINAL   ACC_SYNTHETIC,
                 lambdaClassName, null,
                 JAVA_LANG_OBJECT, interfaces);

        // Generate final fields to be filled in by constructor
        for (int i = 0; i < argDescs.length; i  ) {
            FieldVisitor fv = cw.visitField(ACC_PRIVATE   ACC_FINAL,
                                            argNames[i],
                                            argDescs[i],
                                            null, null);
            fv.visitEnd();
        }
        …………
  1. 方法名
  2. 签名--参数列表和返回值
  3. 定义方法的类
  4. 方法体

从字节码看lambda可以追溯到源头,所以也就能理解运行时的内存模型。

3.2 LambdaMetafactory.metafactory()调用

源码面前,不是了无秘密吗hhh,点进源码看看这个LambdaMetafactory到底做了什么:

     */    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();    }

它什么也没做,做事的是InnerClassLambdaMetafactory.buildCallSite()创建的最后CallSite,那就进一步看看InnerClassLambdaMetafactory.buildCallSite()

    @Override    CallSite buildCallSite() throws LambdaConversionException {        // 1. 创建生成的类对象        final Class<?> innerClass = spinInnerClass();        if (invokedType.parameterCount {            // 2. 用反射获取构造函数            final Constructor<?>[] ctrs = AccessController.doPrivileged(                    new PrivilegedAction<Constructor<?>[]>() {                @Override                public Constructor<?>[] run() {                    Constructor<?>[] ctrs = innerClass.getDeclaredConstructors();                    if (ctrs.length == 1) {                        // The lambda implementing inner class constructor is private, set                        // it accessible  before creating the constant sole instance                        ctrs[0].setAccessible;                    }                    return ctrs;                }                    });            if (ctrs.length != 1) {                throw new LambdaConversionException("Expected one lambda constructor for "                          innerClass.getCanonicalName()   ", got "   ctrs.length);            }            try {                // 3. 创建实例                 Object inst = ctrs[0].newInstance();                // 4. 根据实例和samBase生成MethodHandle                // 5. 生成ConstantCallSite                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);            }        }    }

首先它生成一个.class文件,虚拟机默认不会输出,需要下面设置VM option-Djdk.internal.lambda.dumpProxyClasses=.,Dump出虚拟机生成的类我得到的是:

import java.lang.invoke.LambdaForm.Hidden;// $FF: synthetic classfinal class MethodReference$$Lambda$1 implements Encode {    private MethodReference$$Lambda$1() {    }    @Hidden    public void encode(Derive var1) {        var1).encrypt();    }}

该类实现了传来的接口函数(动态类生成,熟悉spring的朋友应该很熟悉)。

回到buildCallSite()源码,它使用MethodHandles.constant(samBase, inst)创建MethdHandle,放到CallSite里面,完成整个LambdaMetafactory的工作。
MethodHandles.constant(samBase, inst)相当于一个总是返回inst的方法。

接着就简单看看如何用invokedynamics实现Lambda。别说了,先来几个术语压压惊:
1.dynamic call site
程序中出现lambda的地方都被称作dynamic call site,例如:

理解 MethodHandle的一种方式就是将其视为以安全、现代的方式来实现反射的核心功能。

其余字节码指令解析

3. java.lang.invoke.LambdaMetafactory

先说说LambdaMetafactory有什么用。javadoc给出的解释是:

Facilitates the creation of simple "function objects" that implement one or more interfaces by delegation to a provided MethodHandle, after appropriate type adaptation and partial evaluation of arguments. Typically used as a bootstrap method for invokedynamic call sites, to support the lambda expression and method reference expression features of the Java Programming Language.
When the target of the CallSite returned from this method is invoked, the resulting function objects are instances of a class which implements the interface named by the return type of invokedType, declares a method with the name given by invokedName and the signature given by samMethodType. It may also override additional methods from Object.

LambdaMetafactory方便我们创建简单的"函数对象",这些函数对象通过代理MethodHandle实现了一些接口。
当这个函数返回的CallSite被调用的时候,会产生一个类的实例,该类还实现了一些方法,具体由参数给出

将上面得到的MethodHandle写得更可读就是调用的这个方法:

   public static CallSite LambdaMetafactory.metafactory(MethodHandles.Lookup caller,                                       String invokedName,                                       MethodType invokedType,                                       MethodType samMethodType,                                       MethodHandle implMethod,                                       MethodType instantiatedMethodType);

六个参数,慢慢来。

 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();
    }
Object rcvr = "a";try { MethodType mt = MethodType.methodType(int.class); // 方法签名 MethodHandles.Lookup l = MethodHandles.lookup(); // 调用者,也就是当前类。调用者决定有没有权限能访问到方法 MethodHandle mh = l.findVirtual(rcvr.getClass(), "hashCode", mt); //分别是定义方法的类,方法名,签名 int ret; try { ret = mh.invoke; // 代码,第一个参数就是接收者 System.out.println; } catch (Throwable t) { t.printStackTrace(); }} catch (IllegalArgumentException | NoSuchMethodException | SecurityException e) { e.printStackTrace();} catch (IllegalAccessException x) { x.printStackTrace();}

在Class文件中,方法调用即是对常量池(ConstantPool)属性表中的一个符号引用,在类加载的解析期或者运行时才能确定直接引用。

  1. MethodHandles.Lookup caller代表了一个能访问调用者的lookup context(在这里调用者是CapturedLambda)这个参数在调用时由VM自动压栈;
  2. String invokedName表示要实现的方法名,在这里就是Function的apply方法,调用时由VM自动压栈;
  3. MethodType invokedType告知了上面call site object它所持有的MethodHandle需要的参数和返回类型(signature),在这里是(Llambda/CapturedLambda;Ljava/lang/String;)Ljava/util/function/Function因为captureValue方法里面的表达式使用到了一个局部变量prefix和成员变量name所以有这2个参数,调用时由VM自动压栈;
  4. MethodType samMethodType表示要实现functional interface里面抽象方法的类型,在这里是Function的apply方法,即(Ljava/lang/Object;)Ljava/lang/Object;
  5. MethodHandle implMethod表示要调用的desugared method,这里就是lambda/CapturedLambda.lambda$captureValue$0(Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object;
  6. MethodType instantiatedMethodType即运行时的类型,因为方法定义可能是泛型,传入时可能是具体类型String之类的,要做类型校验强转等等,可以与MethodType samMethodType相同,这里是一样的,都是 (Ljava/lang/Object;)Ljava/lang/Object

一个 java 方法的实体有四个构成:

用verbose命令看一下方法主体的字节码信息,这里暂时省略常量池信息,后面会在符号引用到常量池信息的地方具体展示。

 …………
generateConstructor();

        if (invokedType.parameterCount() != 0) {
            generateFactory();
        }

        // Forward the SAM method
        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, samMethodName,
                                          samMethodType.toMethodDescriptorString(), null, null);
        mv.visitAnnotation("Ljava/lang/invoke/LambdaForm$Hidden;", true);
        new ForwardingMethodGenerator(mv).generate(samMethodType);
 …………

用 MethodHandle 实现的方法调用的示例如下,可以看到方法的四个构成:

在运行时期,虚拟机会通过调用这个方法来返回一个CallSite(调用点)对象。简述一下方法的执行过程,首先,初始化一个InnerClassLambdaMetafactory对象,这个对象的buildCallSite方法会将Lambda表达式先转化成一个内部类,这个内部类是MethodHandles.Lookup caller的一个内部类,也即包含此Lambda表达式的类的内部类。这个内部类是通过字节码生成技术(jdk.internal.org.objectweb.asm)生成,再通过UNSAFE类加载到JVM。然后再返回绑定此内部类的CallSite对象,这个过程的源码也可以看一下:

java里的Lambda Expression想必大家都已经很清楚是个什么东西了(什么?不清楚?不清楚我也不多BB)。Lambda Expression就是被解析为functional interface的实现类,实现了它有且仅有的一个抽象方法(exactly one abstract method)。

MethodType 表示方法签名。

责任编辑:

“生”完收工~ 最后用UNSAFE.defineAnonymousClass加载了类,这个方法每次都会返回不同的类。还有,看到了dumper这是为了调试用的,可以将生成了类保存到指定目录下,通过-Djdk.internal.lambda.dumpProxyClasses=目录

同一个类中,方法名相同,签名不同,JVM 会视为不同的方法,不过在 Java 中只支持签名的参数列表部分,也就是重载多态。一次方法调用,除了要方法的实体外,还要调用者和接收者,调用者也就是方法调用语句所在的类。接收者是一个对象,每个方法调用都要一个接收者,它可以是隐藏的,也可以是类方法,比如: String.valueOf,类也是 Class 的一个实例。

这个方法的前三个参数都是由JVM自动链接Call Site生成。方法最后返回一个CallSite对象,对应invokedynamic指令的操作数。

够了够了,直接来看Lambda在编译期做的事儿。首先会有个desugar的操作,就是把Lambda body转换成一个方法,方法有跟Lambda Expression对应的参数和返回值(可能会有额外的参数,比如你用到了外部变量)。生成的方法就叫做desugared method,方法大体可以分为两种类型:一种叫non-instance-capturing,意思是表达式里没有用到外部引用(this, super或者外部类的成员属性),这种会被转换为私有的静态方法;另外一种就是instance-capturing,跟上一个正好相反,会被转换成私有的实例方法。比如说这里有个表达式:

最后

没有相关经验,inDy 还是很不好理解的,学习了 java 8 和 groovy 对 inDy 的应用才有一点大致的认识,文中如果有什么错误,还请帮忙指出。

原文链接:

  • tag: 占用一个字节(u1)的tag,也即InvokeDynamic的一个标记值,其会转化成一个字节的tag值。可以看一下jvm spec中,常量池的tag值转化表(这里tag值对应=18):

Expression在java中提出是在JSR 335中,那么自然而然,要实现这个玩意儿就会有几种方法,比如说内部类,动态代理之类的。但是这里有两个重要目标:

详细可参考:

方法调用的字节码指令

 static {
        final String key = "jdk.internal.lambda.dumpProxyClasses";
        String path = AccessController.doPrivileged(
                new GetPropertyAction(key), null,
                new PropertyPermission(key , "read"));
        dumper = (null == path) ? null : ProxyClassesDumper.getInstance(path);
    }

先看一下第一行字节码指令信息

 private static synthetic lambda$main$0()V
 方法体省略。。。
  1. 第九条:21: return方法返回,因为是void方法,所以就是opcode就是return。此时操作数栈和局部变量表都是空,方法返回。最后再画上一笔:
 INVOKEDYNAMIC apply(Llambda/CapturedLambda;Ljava/lang/String;)Ljava/util/function/Function; [
      // handle kind 0x6 : 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;
      // arguments:
      (Ljava/lang/Object;)Ljava/lang/Object;, 
      // handle kind 0x7 : INVOKESPECIAL
      lambda/CapturedLambda.lambda$captureValue$0(Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object;, 
      (Ljava/lang/Object;)Ljava/lang/Object;
    ]

0: invokedynamic #2, 0

spinInnerClass方法是具体用ASM组装匿名类的地方,我不想贴这么多代码。。。能省略还是省略吧:

通过字节码分析JDK8中Lambda表达式编译及执行机制【面试 工作】

 …………
cw.visitEnd();

        // Define the generated class in this VM.

        final byte[] classBytes = cw.toByteArray();

        // If requested, dump out to a file for debugging purposes
        if (dumper != null) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                @Override
                public Void run() {
                    dumper.dumpClass(lambdaClassName, classBytes);
                    return null;
                }
            }, null,
            new FilePermission("<<ALL FILES>>", "read, write"),
            // createDirectories may need it
            new PropertyPermission("user.dir", "read"));
        }

        return UNSAFE.defineAnonymousClass(targetClass, classBytes, null);

www.3559.com 1

在这里invokedynamic所需要的参数是两个在run-time constant pool里的索引,它俩合起来指向的是一个CONSTANT_InvokeDynamic_info结构,在这里工具帮我展开了。然后结合之前的metafactory方法看一下参数的对应:

www.3559.com 2

根据有没有构造函数参数来创建不同的CallSite,如果有直接将构造函数包装成MethodHandle作为CallSite的target,否则就运用
Lookup来查找工厂方法作为target。最后看一下生成的lambda class:

lambda表达式运行机制

2.bootstrap method
java里对所有Lambda的有统一的bootstrap方法(LambdaMetafactory.metafactory):

以上可以看出Consumer接口的泛型被擦除(编译期间进行,所以字节码信息中并不会包含泛型信息),所以这里并不知道实际的参数操作数类型。但是这里可以得到实际对象的引用值,这里accept方法执行,greeter和person引用出栈,如下图:

通过INVOKEINTERFACE 来完成对functional interface object的调用。

invokedynamic指令特性

编译之后就会在同类生成对应的non-instance-capturing方法:

www.3559.com 3

做了一些为用ASM生成匿名类所要用到的字段,类名,接口名等等的一些准备工作,然后回来看buildCallSite方法:

www.3559.com 4

在这里生成了头部信息,即类名(根据它的生成规则,在文中的例子里是CapturedLambda$$Lambda$1),还有要实现的接口(这里是Function接口)还有访问标识乱七八糟什么的,接着生成实例字段。ASM的文档可以看ASM API。继续看~

这个过程将生成一个代表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的过程。

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            System.out.printf("Im a stateless lambda");
        }).start();
        Thread.sleep(100);
    }

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。

private getValue(Ljava/util/function/Function;Ljava/lang/String;)Ljava/lang/String;
   L0
    LINENUMBER 27 L0
    ALOAD 1
    ALOAD 2
    INVOKEINTERFACE java/util/function/Function.apply (Ljava/lang/Object;)Ljava/lang/Object;
    …………

- name_and_type_index:代表常量池表信息的一个有效索引值,其指向的常量池属性表结构一定是一个CONSTANT_NameAndType_info属性,代表了方法名称和方法描述符信息。再沿着 #44索引看一下常量池相关项的描述内容:

它new了一个InnerClassLambdaMetafactory实例,然后返回对应的CallSite对象,接着看InnerClassLambdaMetafactory:

CONSTANT_InvokeDynamic_info结构如下:

1.maximizing flexibility for future optimization by not committing to a specific strategy;2.providing stability in the classfile representation.

结合CONSTANT_InvokeDynamic_info的结构信息来看一下这个常量池表项包含的信息。

final class CapturedLambda$$Lambda$1 implements Function {
    private final CapturedLambda arg$1;
    private final String arg$2;

    private CapturedLambda$$Lambda$1(CapturedLambda var1, String var2) {
        this.arg$1 = var1;
        this.arg$2 = var2;
    }

    private static Function get$Lambda(CapturedLambda var0, String var1) {
        return new CapturedLambda$$Lambda$1(var0, var1);
    }

    @Hidden
    public Object apply(Object var1) {
        return this.arg$1.lambda$captureValue$0(this.arg$2, var1);
    }
}

通过打印consumer对象的className(greeter.getClass().getName())可以得到结果是eight.Functionnal$$Lambda$1/659748578前面字符是Lambda表达式的ClassName,后面的659748578是刚才所述内部类的hashcode值。

    public static CallSite metafactory(MethodHandles.Lookup caller,
                                       String invokedName,
                                       MethodType invokedType,
                                       MethodType samMethodType,
                                       MethodHandle implMethod,
                                       MethodType instantiatedMethodType)
  1. 第七条:13: invokespecial #5初始化Person对象的指令(#5指向了常量池Person的初始化方法eight/Person.””:(Ljava/lang/String;)V),也即调用Person构造函数的指令。此时”Lambda”常量池的引用以及 dup复制的person引用地址出操作数栈。这条指令执行之后,才在堆中真正创建了一个Person对象。此时栈帧结构如下:
public class CapturedLambda{
   ……
   public String captureValue(){
        String prefix="weirdness";
       return getValue(arg -> arg prefix name,"1");
    }
    private <T extends String> T getValue(Function func,String num){
        return (T) func.apply(num);
    }
   ……
}

www.3559.com 5

这两个目标看起来似乎互不兼容,但是使用invokedynamic instruction来实现,就可以完美解决。Lambda Expression依赖了一些JSR 292中的特性,比如invokedynamic和method handles等等...

lambda表达式字节码指令示例分析

实现了Function接口,构造函数接收了外部引用,有刚才说的绑定到CallSite的工厂方法,实现的apply调用的之前编译器生成的instance-capturing方法:

  1. 第二条指令:5: astore_1 指令起始偏移位置是5,主要取决于前面一个指令(invokedynamic)有两个操作数,每个操作数占两个字节(u2)空间,所以第二条指令就是从字节偏移位置5开始(后续的偏移地址将不再解释)。此指令执行后,当前方法的栈帧结构如下(注:此图没有画出当前栈帧的动态链接以及返回地址的数据结构,图中:左侧局部变量表,右侧操作数栈):
 public InnerClassLambdaMetafactory(MethodHandles.Lookup caller,
                                       MethodType invokedType,
                                       String samMethodName,
                                       MethodType samMethodType,
                                       MethodHandle implMethod,
                                       MethodType instantiatedMethodType,
                                       boolean isSerializable,
                                       Class<?>[] markerInterfaces,
                                       MethodType[] additionalBridges)
            throws LambdaConversionException {
        super(caller, invokedType, samMethodName, samMethodType,
              implMethod, instantiatedMethodType,
              isSerializable, markerInterfaces, additionalBridges);
        implMethodClassName = implDefiningClass.getName().replace('.', '/');
        implMethodName = implInfo.getName();
        implMethodDesc = implMethodType.toMethodDescriptorString();
        implMethodReturnClass = (implKind == MethodHandleInfo.REF_newInvokeSpecial)
                ? implDefiningClass
                : implMethodType.returnType();
        constructorType = invokedType.changeReturnType(Void.TYPE);
        lambdaClassName = targetClass.getName().replace('.', '/')   "$$Lambda$"   counter.incrementAndGet();
        cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        …………

这段字节码信息展示了,引导方法就是LambdaMetafactory.metafactory方法。对照着前面LambdaMetafactory.metafactory的源码一起阅读。通过debug先看一下这个方法在运行时的参数值:

结语

本文简单描述了Lambda Expression一小部分的转换和执行过程,如果有错误地方,还望无情指出。
本文其中还未涉及很多细节以及其他Lambda的特性,比如method reference capture,不只是一段表达式要被转换,一个new语句或者方法调用语句也要转换:

list.filter(String::isEmpty)

还比如AdaptationsSerialization(PS:好吧...这个我没用到,还不是太了解,手动滑稽),再比如Call Site Specifier的解析过程等等等等等......
如果想了解我上面所说的种种可以参考以下资料:

  1. http://cr.openjdk.java.net/~briangoetz/lambda/lambda-translation.html
  2. http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.invokedynamic
  3. http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4.10
  4. http://docs.oracle.com/javase/specs/jls/se9/html/jls-15.html#jls-15.27
  5. http://stackoverflow.com/questions/30934890/java-heap-dump-analysis-for-lambda-expressions
  6. https://blogs.oracle.com/jrose/anonymous-classes-in-the-vm

-----------------------------------------------------------------------------------------------------------------------------------------------作者:朔某人

  1. 第六条:11: ldc #4将运行时常量池的值入操作数栈,这里的值是Lambda字符串。#4在常量池属性表中结构信息如下:
    new Thread(() -> {
            System.out.printf("Im a stateless lambda");
        }).start();

www.3559.com 6

synthetic flag表示方法不在源码中展示。再看一个使用到了成员属性的表达式:

原标题:通过字节码分析JDK8中Lambda表达式编译及执行机制【面试 工作】

生成私有构造方法和如有必要生成工厂方法。在文中这个例子里需要生成工厂方法,因为用到了外部变量,这个是需要运行时传过来的,但比如是生成的是non-instance-capturing方法就不需要,因为没用到外部变量直接用构造函数。接着看~

  1. 第四条:7: new #3初始化person对象指令,这里并不等同于new关键字,new操作码只是找到常量池的符号引用,执行到此行命令时,运行时堆区会创建一个有默认值的对象,如果是Object类型,那么默认值是null,然后将这个对于默认值的引用地址压入到操作数栈。其中#3操作数指向的常量池Class属性表的一个引用,可以看到这个常量池项为:#3 = Class #45 // eight/Person。此时的运行时栈帧结构如下:

bootstrap运行期动态生成了匿名类,将其与CallSite绑定,得到了一个获取匿名类实例的call site object。
3.call site object
call site object持有MethodHandle的引用作为它的target,它是bootstrap method方法成功调用后的结果,将会与 dynamic call site永久绑定。call site object的target会被JVM执行,就如同执行一条invokevirtual指令,其所需的参数也会被压入operand stack。最后会得一个实现了functional interface的对象。

可以看到第一条指令就是代表了lambda表达式的实现指令,invokedynamic指令,这个指令是JSR-292开始应用的规范,而鉴于兼容和扩展的考虑(可以参考Oracle工程师对于使用invokedynamic指令的原因),JSR-337通过这个指令来实现了lambda表达式。也就是说,只要有一个lambda表达式,就会对应一个invokedynamic指令。

private synthetic lambda$captureValue$0(Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object;
方法体省略。。。

关于方法调用的其他详细的解释可以参考官方文档《The Java® Virtual Machine Specification Java8 Edition》-2.11.8 Method Invocation and Return Instructions。

   CallSite buildCallSite() throws LambdaConversionException {
        final Class<?> innerClass = spinInnerClass();
        …………
    }

www.3559.com 7

ok,再回到buildCallSite方法看一下余下的工作:

  1. 第五条:10: dup复制操作数栈栈顶的值,并且将该值入操作数栈栈顶。dup指令是一种对于初始化过程的编译期优化。因前面的new操作码并不会真正的创建对象,而是push一个引用到操作数栈,所以dup之后,这个栈顶的复制引用就可以用来给调用初始化方法(构造函数)的invokespecial提供操作数时消耗掉,同时原有的引用值就可以给其他比如对象引用的操作码使用。此时栈帧结构如下图:

在invokedynamic调用完成得到CapturedLambda$$Lambda$1实例之后,就可以完成最后一步操作:

www.3559.com 8

private synthetic lambda$captureValue$0(Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/Object;
方法体省略。。。

www.3559.com 9

综上,已经介绍了lombda表达式在字节码上的实现方式。其他指令,如果对字节码指令感兴趣可以继续阅读,已经了解的可以略过,本小节和lambda本身没有太大关联。

在看字节码细节之前,先来了解一下lambda表达式如何脱糖(desugar)。lambda的语法糖在编译后的字节流Class文件中,会通过invokedynamic指令指向一个bootstrap方法(下文中部分会称作“引导方法”),这个方法就是java.lang.invoke.LambdaMetafactory中的一个静态方法。通过debug的方式,就可以看到该方法的执行,此方法源码如下:

www.3559.com 10

www.3559.com 11

Lambda不仅用起来很方便,性能表现在多数情况也比匿名内部类好,性能方面可以参考一下Oracle的Sergey Kuksenko发布的 Lambda 性能报告。由上文可知,虽然在运行时需要转化Lambda Form(见MethodHandle的form属性生成过程),并且生成CallSite,但是随着调用点被频繁调用,通过JIT编译优化等,性能会有明显提升。并且,运行时脱糖也增强了编译期的灵活性(其实在看字节码之前,一直以为Lambda可能是在编译期脱糖成一个匿名内部类的Class,而不是通过提供一个boortrap方法,在运行时链接到调用点)。运行时生成调用点的方式实际的内存使用率在多数情况也是低于匿名内部类(java8 之前版本的写法)的方式。所以,在能使用lambda表达式的地方,我们尽量结合实际的性能测试情况,写简洁的表达式,尽量减少Lambda表达式内部捕获变量(因为这样会创建额外的变量对象),如果需要在表达式内部捕获变量,可以考虑是否可以将变量写成类的成员变量,也即尽量少给Lambda传多余的参数。希望本文能给Lambda的使用者一些参考。返回搜狐,查看更多

  • bootstrap_method_attr_index:指向bootstrap_methods的一个有效索引值,其结构在属性表的 bootstrap method 结构中,也描述在Class文件的二进制字节流信息里。下面是对应索引 0 的bootstrap method 属性表的内容:

www.3559.com 12

  1. invokestatic 主要用于调用static关键字标记的静态方法
  2. invokespecial 主要用于调用私有方法,构造器,父类方法。
  3. invokevirtual 虚方法,不确定调用那一个实现类,比如Java中的重写的方法调用。
  4. invokeinterface 接口方法,运行时才能确定实现接口的对象,也就是运行时确定方法的直接引用,而不是解析期间。
  5. invokedynamic 这个操作码的执行方法会关联到一个动态调用点对象(Call Site object),这个call site 对象会指向一个具体的bootstrap 方法(方法的二进制字节流信息在BootstrapMethods属性表中)的执行,invokedynamic指令的调用会有一个独特的调用链,不像其他四个指令会直接调用方法,在实际的运行过程也相对前四个更加复杂。结合后面的例子,应该会比较直观的理解这个指令。

先看一个简单的示例,示例使用了java.util.function包下面的Consumer。

本文只是通过Consumer接口分析lambda表达式的字节码指令,以及运行时的脱糖过程。也是把操作码忘得差不多了,也顺便再回顾一下。

示例代码:(下面的Person对象只有一个String类型属性:name,以及一个有参构造方法)

www.3559.com 13

www.3559.com 14

www.3559.com 15

www.3559.com 16

www.3559.com 17

常量池索引位置#2的信息如下:

  1. 第三条: 6: aload_1将greeter 弹出局部变量表,压入操作数栈。

www.3559.com 18

  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),以及动态的调用方法名和返回信息。

www.3559.com 19

www.3559.com 20

www.3559.com 21

结语

7.第八条:16: invokeinterface #6, 2调用了Consumer的accept接口方法{greeter.accept(person)}。#6逗号后面的参数2是invokeinterface指令的参数,含义是接口方法的参数的个数加1,因为accpet方法只有一个参数,所以这里是1 1=2。接着再看一下常量池项 #6属性表信息:

编辑:www.3559.com 本文来源:和invokedynamic指令详细分析,通过字节码分析JDK

关键词: www.3559.com