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

方法引用(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对象的呢?

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

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给出)

方法调用的字节码指令

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

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

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);

六个参数,慢慢来。

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

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一样

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

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的方法。

lambda表达式运行机制

总结

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

  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

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

澳门金沙在线官网 1

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

澳门金沙在线官网 2

澳门金沙在线官网 3

这个过程将生成一个代表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,以及一个有参构造方法)

澳门金沙在线官网 4

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

澳门金沙在线官网 5

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的信息如下:

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

CONSTANT_InvokeDynamic_info结构如下:

澳门金沙在线官网 6

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

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

澳门金沙在线官网 7

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

澳门金沙在线官网 8

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

澳门金沙在线官网 9

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


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

澳门金沙在线官网 10

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

其余字节码指令解析

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

  1. 第二条指令:5: astore_1
    指令起始偏移位置是5,主要取决于前面一个指令(invokedynamic)有两个操作数,每个操作数占两个字节(u2)空间,所以第二条指令就是从字节偏移位置5开始(后续的偏移地址将不再解释)。此指令执行后,当前方法的栈帧结构如下(注:此图没有画出当前栈帧的动态链接以及返回地址的数据结构,图中:左侧局部变量表,右侧操作数栈):

澳门金沙在线官网 11

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

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

澳门金沙在线官网 12

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

澳门金沙在线官网 13

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

澳门金沙在线官网 14

  1. 第六条:11: ldc #4将运行时常量池的值入操作数栈,这里的值是Lambda字符串。#4在常量池属性表中结构信息如下:

澳门金沙在线官网 15

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

澳门金沙在线官网 16

  1. 第七条:13: invokespecial #5初始化Person对象的指令(#5指向了常量池Person的初始化方法eight/Person.””:(Ljava/lang/String;)V),也即调用Person构造函数的指令。此时”Lambda”常量池的引用以及
    dup复制的person引用地址出操作数栈。这条指令执行之后,才在堆中真正创建了一个Person对象。此时栈帧结构如下:

澳门金沙在线官网 17

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

澳门金沙在线官网 18

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

澳门金沙在线官网 19

  1. 第九条:21: return方法返回,因为是void方法,所以就是opcode就是return。此时操作数栈和局部变量表都是空,方法返回。最后再画上一笔:

澳门金沙在线官网 20

结语

本文只是通过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。

澳门金沙在线官网 21

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

责任编辑:

相关文章