Invokedynamic

作者: 请输入妮称 | 来源:发表于2019-02-24 00:05 被阅读0次

    Invokedynamic指令是java7中加入的字节码指令,理解这条指令可以让我们熟悉程序的执行流程,这篇文章将会介绍invokedynamic指令解决了什么问题以及是如何解决的。

    Method handles

    Method handles通常被认为是对反射api的包装。这么描述是不准确的,虽然Method handles可以调用到method,constructor,field,但是并不持有这些属性的描述信息,比如方法的描述符(公开还是私有)、方法的注解是无法获取到的。可以把Method handles理解为一个残缺的反射api。

    Method handles不能直接初始化,可以使用MethodHandles类提供的工厂方法。

    MethodHandles.Lookup lookup = MethodHandles.lookup()
    

    当以上方法调用的时候,首先会创建一个安全的上下文环境,使得lookup对象仅仅只能定位到对当前类可见的属性。如下:

    class Example {
     void doSomething() {
     MethodHandles.Lookup lookup = MethodHandles.lookup();
     }
     private void foo() { /* ... */ }
    }
    

    lookup对象仅仅能定位到对Example类可见的属性,比如说foo方法。其他类中对Example类不可见的私有方法是定位不到的。而反射api不care这种限制,这里存在两者的不同。

    此外,一个Method handle只能持有一个指定具体类型的方法。方法的类型包括返回类型和参数类型。

    可以使用MethodType来描述方法的类型。

    class Counter {
     static int count(String name) {
     return name.length();
     }
    }
    

    Counter类的count方法可以按照如下方式来创建MethodType

    MethodType methodType = MethodType.methodType(int.class, new Class<?>[] {String.clas})
    

    通过上面创建的lookupmethodType,可以用来定位 Counter类的count方法

    MethodType methodType = MethodType.methodType(int.class, new Class<?>[] {String.class});
    
    MethodHandles.Lookup lookup = MethodHandles.lookup();
    
    MethodHandle methodHandle = lookup.findStatic(Counter.class, "count", methodType);
    
    int count = methodHandle.invokeExact("foo");
    
    assertThat(count, is(3));
    

    虽然看起来比反射复杂的多,但是以上示例并不是Method Handles的主要用途。
    Method Handle和反射的主要区别,可以通过观察编译后的字节码来了解。
    Java中每个方法都有一个独特的方法签名,签名由方法名、方法参数组成。虽然语言层面上不允许通过改变方法返回类型来进行方法的重载,但是字节码层面是允许的。

    当通过反射Method.invoke()调用时,无论参数传递的是何种类型,都会调用方法签名为
    invoke(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;的方法,也即调用的是同一个方法。

    当使用MethodHandle. invokeExact ()调用时,编译器会根据传入的参数和返回值生成具体的方法,方法参数或者返回类型不同,调用的方法也是不一样的。当实际的返回值和期待的返回值不一致时,会抛出运行时异常。

    依然以调用Counter类的count方法为例

    int count1 = methodHandle.invokeExact((Object) "foo");
    int count2 = (Integer) methodHandle.invokeExact("foo");
    methodHandle.invokeExact("foo");
    

    这三条语句均会抛出运行时异常。第一条语句是参数类型不匹配,第二条语句是返回类型不匹配,第三条语句是因为没有指定返回值的话,编译器默认方法返回值为void,同样是返回类型不匹配。

    如果觉得invokeExact方法太过严苛,可以使用invoke方法来替代,这样可以进行自动类型转换和封箱、拆箱操作。

    Fields, methods 和 constructors 合并归一

    method handles可以触达到的属性不仅仅局限于method,还包括构造方法和变量。
    MethodHandle并不在意调用的是方法还是变量,只需要MethodType对象和属性类型相匹配即可。

    使用MethodHandles.Lookup对象,可以获取对变量的引用。如果想要设置一个变量可以使用findSetter方法,想要读取一个变量,可以使用findGetter方法。

    public class Bean {
        String value;
    
        void print(String x) {
            System.out.println(x);
        }
    }
    
    MethodHandle fieldHandle = lookup.findSetter(Bean.class, "value", String.class);
    MethodType methodType = MethodType.methodType(void.class, new Class<?>[] {String.class});
    MethodHandle methodHandle = lookup.findVirtual(Bean.class, "print", methodType);
    

    fieldHandle和methodHandle都可以调用相同的invokeExact方法。

    anyHandle.invokeExact((Bean) mybean, (String) myString);
    

    注意到上边第一个参数,第一个参数是bean对象,这是因为对于非静态方法的调用,在字节码层面,bean对象会被当作第一个参数传递进去。在java代码层面来看,非静态方法的调用,this对象会被当作隐式参数,放在参数列表的第一位传递进去。每个非静态方法都持有当前对象的引用。

    比起反射更强大的是,Method handles还可以调用到父类的方法。

    性能

    当使用MethodHandle. invokeExact ()调用时,编译器会根据传入的参数和返回值生成具体的方法,方法参数或者返回类型不同,调用的方法也是不一样的。和反射相比,少了封箱、拆箱操作,因此会提高一点性能。

    创建invokedynamic调用点

    java8中的lambda表达式在编译成字节码时会生成invokedynamic调用点。虽然lambda表达式也可以通过转换成匿名内部类来解决调用问题,但是使用invokedynamic推迟了类似class的创建。现在我们仅仅先讨论invokedynamic如何在运行期进行方法的分派。

    为了更好的理解invokedynamic调用点,使用byte-buddy可以帮助我们窥视invokedynamic的实现机制,它可以实现invokedynamic的字节码织入功能,并且不需要我们非常了解字节码的格式。

    每个调用点最终都会获取到一个MethodHandle对象,该对象描述了想要调用的方法。当执行到invokedynamic调用点时,会由java虚拟机自动执行调用流程,而且会对调用过程进行优化。执行过程中,会执行bootstrap方法(该方法是用户自定义的),获取到MethodHandle对象,可以看一下bootstrap实现例子:

    class Bootstrapper {
      public static CallSite bootstrap(Object... args) throws Throwable {
        MethodType methodType = MethodType.methodType(int.class, new Class<?>[] {String.class})
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        MethodHandle methodHandle = lookup.findStatic(Counter.class, "count", methodType);
        return new ConstantCallSite(methodHandle);
      }
    }
    

    这个例子里,我们先不关注方法的入参信息。可以看到该方法是静态的(这是强制要求),每个invokedynamic调用点都会调用该bootstrap方法,接下来的执行流程则交由用户程序控制。当bootstrap方法返回后,执行流程交回虚拟机,虚拟机根据返回的MethodHandle信息,执行实际的方法。

    从以上bootstrap方法可以看出,MethodHandle对象不是直接返回的,而是由CallSite对象包装了一下。这么做有一个好处,这样CallSite里包装的MethodHandle对象可以随时替换。CallSite有不同的实现类,我们这个方法里返回的是ConstantCallSite,其中的MethodHandle是不可以被替换的,而MutableCallSite里的MethodHandle是可以被替换的。

    使用以上提供的bootstrap方法和byte-buddy,我们现在可以自定义invokedynamic调用逻辑。

    我们首先定义一个抽象类:

    abstract class Example {
     abstract int method();
    }
    

    接下来借助byte-buddy实现这个抽象类,在method方法的实现里,包含一个invokedynamic调用点。byte-buddy会生成一个类似方法签名的method方法,只不过对于非静态方法,会增加this为第一个参数。

    假设我们在invokedynamic调用点处想调用Counter.count()方法,我们需要创建一个调用点,该调用点接收一个String类型的参数:

    Instrumentation invokeDynamic = InvokeDynamic
     .bootstrap(Bootstrapper.class.getDeclaredMethod(“bootstrap”, Object[].class))
     .withoutImplicitArguments()
     .withValue("foo");
    

    byte-buddy提供了InvokeDynamic类,该类接收一个bootstrap方法,这里通过反射拿到了我们自定义的bootstrap方法的句柄。

    接下来创建一个Example实现类:

    Example example = new ByteBuddy()
     .subclass(Example.class)
     .method(named(“method”)).intercept(invokeDynamic)
     .make()
     .load(Example.class.getClassLoader(), 
     ClassLoadingStrategy.Default.INJECTION)
     .getLoaded()
     .newInstance();
    int result = example.method();
    assertThat(result, is(3));
    

    通过设置执行断点,确实可以看到最终执行了Counter.count()方法。

    截止到目前,我们还没有看到invokedynamic的强大之处,我们仅仅绑定了Counter.count()方法。借助于bootstrap方法的入参,我们可以实现更灵活的功能。

    bootstrap方法接收至少三个参数,第一个参数是MethodHandles.Lookup对象,该对象包含了一个安全的上下文,可以用来搜索实际的调用方法。第二个参数是String对象,表示要绑定的方法的名称,这个参数可以不严格遵守,我们可以传入“A”方法却最终调用“B”方法,毕竟bootstrap方法的实现是由我们决定的。第三个参数是MethodType对象,描述了我们想要绑定的方法的入参,返回值信息。

    除了以上三个参数,我们还可以传递多余的参数,这些参数可以当作绑定方法的入参。

    其他多用的参数是什么类型可以由bootstrap方法自行决定,如果bootstrap方法可以接收Object类型的可变数组对象,那么则可以接收传递进来的任何参数,这就是为什么在以上例子中可以传递一个String参数。

    bootstrap方法接收的参数类型是有限制的,只能是以下几种:

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

    Lambda表达式

    当编译lambda方法时,编译器会创建一个class类,把labmda方法体放置在类中的私有方法里,方法的命名按如下所示的格式:

    lambda$X$Y
    

    "X"指代声明lambda所在的方法名称,“Y”是一个从0开始递增的序列号。
    方法体的参数是lambda表达式所实现的接口方法所决定的。鉴于lambda表达式不使用非静态变量和封闭类的方法,所以方法体总是被定义为静态类型。

    lambda表达式被invokedynamic调用点替换。当调用时,调用点首先请求绑定的工厂方法去生成lambda表达式所实现的接口的实例,比如:

    Runnable r = () -> System.out.println("hello lambda");
    

    lambda实现的是Runnable接口,所以调用点会生成一个Runnable接口的实例。

    调用点会提供lambda表达式所实现的接口方法的所有参数。

    任何invokedynamic调用点都会执行到LambdaMetafactory类。该类存在于java类库中,该类可以创建一个lambda实现的接口方法的实例,该实例包含lambda的方法体。在将来,实现类lambda表达式的机制可能会改变,如果存在更好的语言特性去实现lambda表达式,这种实现机制可能被替换掉。

    当调用时,bootstrap方法使用ASM库来创建lambda表达式所对应接口的实现类。举个例子来看一下实现机制。

    class Foo {
     int i;
     void bar(int j) {
     Consumer consumer = k -> System.out.println(i + j + k);
     }
    }
    

    可见lambda隐式的持有Foo的引用和局部变量j,所以生成的代码类似下面这样:

    class Foo {
     int i;
     void bar(int j) {
     Consumer consumer = <invokedynamic(this, j)>;
     }
     private /* non-static */ void lambda$foo$0(int j, int k) {
     System.out.println(this.i + j + k);
     }
    }
    

    lambda表达式的方法体被包装在了lambdafoo0的私有方法里。

    Foo的引用和变量j会传递给invokedyanmic命令所绑定的factory方法,生成的代码如下:

    class Foo$$Lambda$0 implements Consumer {
     private final Foo _this;
     private final int j;
     private Foo$$Lambda$0(Foo _this, int j) {
     this._this = _this;
     this.j = j;
     }
     private static Consumer get$Lambda(Foo _this, int j) {
     return new Foo$$Lambda$0(_this, j);
     }
     public void accept(Object value) { // type erasure
     _this.lambda$foo$0(_this, j, (Integer) value);
     }
    }
    

    最终,根据生成的class类创建“MethodHandle”句柄,该句柄被塞进ConstantCallSite对象里。如果lambda表达式是无状态的(不引用成员变量或其他方法),那么LambdaMetafactory返回一个所谓的“constant” method handle,该方法句柄指向生成类的一个实例,该实例被当作单例来处理,这样每次调用时,不需要重复创建对象,节约内存。

    lambda forms

    Lambda forms是MethodHandles在虚拟机下执行流程的具体实现。lambda froms是受到lambda的启发而产生的,并不是lambda的实现方式。

    在OpenJDK 7的早期版本中,method handles可以选择两种模式中的一种执行。如果method handle可以被视为常量类型,就会转换为对应的字节码,否则就会在运行时动态分发。由于运行时分发不能被JIT优化,所以非常量类型的method handle在性能上是有所损失的。

    LambdaForm是用来解决这一问题的。粗略的说, lambda forms所代表的字节码可以被JIT优化。在OpenJDK,MethodHandle被LambdaForm替换掉了,LambdaForm持有一个指向MethodHandle的引用。LambdaForm是可优化的,因此在非常量类型的MethodHandle变为高性能的调用。在bootstrap方法里或者通过MethodHandle调用的方法里设置断点,可以看到当调用到断点处时,可以在调用栈里发现LambdaForm。

    总结

    任何执行在jvm上的语言,都会被编译为遵守jvm规范的字节码。java是静态类型语言,方法调用有着严格的类型约束,在编译期必须确定被调用方法所归属的class。javaScript是一门动态类型的语言,方法的调用可以在运行时确定:

    function (foo) {
      foo.bar();
    }
    

    使用invokedynamic指令,可以在运行时再决定由哪个类来接收被调用的方法。在此之前,只能使用反射来实现类似的功能。该指令使得可以出现基于JVM的动态语言,让jvm更加强大。而且在JVM上实现动态调用机制,不会影响java本身的发展。

    参考文档:

    https://www.bouvet.no/bouvet-deler/utbrudd/dismantling-invokedynamic

    https://blog.csdn.net/feather_wch/article/details/82719313

    https://www.jianshu.com/p/d74e92f93752

    相关文章

      网友评论

        本文标题:Invokedynamic

        本文链接:https://www.haomeiwen.com/subject/ihtgeqtx.html