从MethodHandle到InvokeDynamic指令

Posted by kingkk on 2020-11-25

前言

之前在用ASM处理字节码的时候,发现rt.jar中居然有比较多的invokedynamic指令,并且ASM对该指令提供了一个单独的结构体来描述InvokeDynamicInsnNode,甚至不能一眼看出这个指令具体调用了个什么方法。

之后便翻了几天文档和一些文章,在这里做一下记录,文章难免有疏漏,欢迎指出。

MethodHandle

MethodHanlde是Java7之后出现的API,以及其相关的类都在java.lang.invoke包中。

文档中对它的定义是这样的

方法句柄是对基础方法,构造函数,字段或类似的低级操作的类型化,直接可执行的引用,并带有自变量或返回值的可选转换。

方法句柄的操作其实和java.lang.reflect中的反射操作很类似,先来看下熟悉的通过反射API执行系统命令的操作。

1
2
3
Method exec = Runtime.class.getMethod("exec", String.class);
Runtime runtime = Runtime.getRuntime();
exec.invoke(runtime, "calc");

通过Class.getMethod方法获取到Runtime类的exec方法之后,通过Method.invoke并传入实例和对应参数,即完成了一次反射调用。

而MethodHandle方法句柄是如何操作的呢?(为了展示清楚,尽可能的将步骤展开成了多步)

1
2
3
4
5
6
MethodType mt = MethodType.methodType(Process.class, String.class);
Runtime runtime = Runtime.getRuntime();
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle handle = lookup.findVirtual(Runtime.class, "exec", mt);
handle = handle.bindTo(runtime);
handle.invoke("calc");

可以明显的看到,原本三行的反射操作,到了MethodHandle这里变成了六行,接下来就是讲解一下每一行的含义。

  • MethodType:查找对应时需要定义其类型,由返回类型和参数的类型决定。这部分信息在反射中在Class.getMethod中传入,且不需要返回值的类型。
  • Runtime.getRuntime():获取Runtime实例
  • MethodHandles.lookup():MethodHandle查找器,可以查找到类的所有方法,如果只想查找public方法可以使用MethodHandles.publicLookup()
  • lookup.findVirtual():查找对应类的方法,传入的参数分别为类、函数名、以及MethodType。除了findVirtual,lookup还有findSpecial、findStatic等方法,分别对应JVM的invokevirtual、invokespecial、invokestatic指令。
  • handle.bindTo():到目前为止,获取到了函数对应的MethodHandle,但必须要绑定到一个实例上之后才可以正常使用,而不是像反射时在具体invoke时再传入。
  • handle.invoke():由于已经绑定了具体的实例,最后一步只要传入对应的参数即可通过MethodHandle调用对应函数。

这样看下来,MethodHandle方法句柄的调用方式明显比reflect反射调用的方式至少在代码层面要繁琐很多。

并且对于MethodHandle而言,没有Method.setAccessible之类的操作,导致private和protected方法只有在类的内部代码中才能使用。甚至使用方法上甚至还要自己指定对于的JVM调用方式(invokevirutal / invokespecial / invokestatic)。

那MethodHandle方法在Java7中引入的意义何在呢?

最大的一个原因是出于性能考虑的,MethodHandle的访问检查是在创建时进行校验的,而不是在实际调用时。这也就意味着生成了一个MethodHandle方法句柄之后,多次调用仅有一次权限检查,而reflect反射会在每次invoke时进行校验。

并且对于JVM而言,可以完全透视MethodHandle并将尝试对其进行优化,从而获得更好的性能。

InvokeDynamic

除了invokevirutal、invokespecial、invokeinterface、invokestatic之外,在Java7发布之后JVM中引入了一条新的调用函数的指令——invokedynamic

它实现了类似于python中”鸭子模型”的功能,为一些在JVM上运行的动态语言提供了动态调用的能力,并在之后的java版本中被运用到一些编译优化的地方。

例如如下是一个Java8中的lambda表达

1
2
3
4
5
6
public class InvokeDynamicTest {
public static void main(String[] args) {
Runnable lambda = () -> System.out.println("hello lambda");
lambda.run();
}
}

反编译之后可以看到main函数开头的第一个函数就是invokedynamic函数

那这个invokedynamic指令究竟是调用了一个什么函数呢?

事实上,invokedynamic指令可能远不止执行一个函数那么简单。

在JVM第一次遇到该invokedynamic指令时,会去调用一个特殊的引导方法(Bootstrap Method, BSM),由引导方法初始化调用过程,返回一个CallSite实例。

如下是文档中对CallSite的定义

CallSite是变量的持有人MethodHandle,称为它的targetinvokedynamic链接到CallSite代表的说明将所有调用委托给该站点的当前目标。

简而言之CallSite就是封装了一个MehtodHandle的调用站点。

再来看下Lambda表达式这个中的例子,是如果调用BSM从而生成CallSite进而进行调用的。

在反编译的invokedynamic指令中可以看到,存在一个#0的引用,该引用就对应了BootstrapMethods中存储的引导方法。

则说明该引导函数会通过java.lang.invoke.LambdaMetafactory.metafactory生成一个对应的CallSite

可以看到metafactory函数提供了六个参数选项,前三个是BSM所必须的,并会由JVM自动传入

  • caller:即调用者,这里是InvokeDynamicTest这个类
  • invokeName:CallSite的调用名,这里是run
  • invokeType:CallSite的的函数签名,这里是()Runnable

后面三个参数则是对应上面反编译结果中的Method arguments。

跟进mf.buildCallSite()之后可以看到,通过spinInnerClass()生成了一个内部类

spinInnerClass()中则是通过ASM动态生成一个字节码,可以dump下来看下

dump下来之后可以看到生成了一个继承了Runnable的匿名类,并且其run方法指向我们InvokeDynamicTest中的lambda匿名函数。

InvokeDynamicTest.lambda$main$0的逻辑其实也很简单,就是我们之前在lambda表达式中写入的System.out.println逻辑。

继续回到buildCallSite逻辑中,生成了这个匿名类之后,通过获取其构造函数,生成一个实例。

并通过MethodHandles.constant生成一个MethodHandle,该方法句柄每次调用时返回固定的常量值,即之前匿名类的实例。最后再封装层CallSite返回。

返回CallSite之后,再具体执行返回的MethodHandle,即得到一个Lambda匿名类实例,并赋值给我们程序中的lambda变量,通过debug也可以验证我们这一点。

最后调用这个匿名类的run方法,进而调用到InvokeDynamicTest中的匿名lambda$main$0函数

更高版本的Java中,编译器利用invokedynamic指令对更多操作进行优化,它们各自有着不同的BSM,例如JDK9中的字符串连接,其对应的BSM为java.lang.invoke.StringConcatFactory.makeConcatWithConstants

但具体原理依旧是和之前的一样,在第一次调用invokedynamic指令时通过BSM创建对应CallSite,之后每次调用直接执行该CallSite中的MethodHandle。

References

https://docs.oracle.com/javase/8/docs/api/java/lang/invoke/MethodHandle.html

https://docs.oracle.com/javase/8/docs/api/java/lang/invoke/CallSite.html

https://www.baeldung.com/java-method-handles

https://www.baeldung.com/java-invoke-dynamic

https://cloud.tencent.com/developer/article/1005920

https://jcp.org/en/jsr/detail?id=292