12

从MethodHandle到InvokeDynamic指令

 3 years ago
source link: https://www.kingkk.com/2020/11/%E4%BB%8EMethodHandle%E5%88%B0InvokeDynamic%E6%8C%87%E4%BB%A4/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

之前在用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函数

20201125163014.png

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

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

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

Untitled-Diagram.svg

如下是文档中对CallSite的定义

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

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

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

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

20201125165056.png

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

20201125165321.png

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

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

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

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

20201125165709.png

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

20201125165946.png

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

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

20201125170232.png

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

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

20201125170418.png

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

20201125170856.png

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

20201125172252.png

更高版本的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



About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK