Invokedynamic

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 = ;
 }
 private /* non-static */ void lambda$foo$0(int j, int k) {
 System.out.println(this.i + j + k);
 }
}

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

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)