前言
之前在用ASM处理字节码的时候,发现rt.jar中居然有比较多的invokedynamic指令,并且ASM对该指令提供了一个单独的结构体来描述InvokeDynamicInsnNode
,甚至不能一眼看出这个指令具体调用了个什么方法。
之后便翻了几天文档和一些文章,在这里做一下记录,文章难免有疏漏,欢迎指出。
MethodHandle
MethodHanlde是Java7之后出现的API,以及其相关的类都在java.lang.invoke
包中。
文档中对它的定义是这样的
方法句柄是对基础方法,构造函数,字段或类似的低级操作的类型化,直接可执行的引用,并带有自变量或返回值的可选转换。
方法句柄的操作其实和java.lang.reflect
中的反射操作很类似,先来看下熟悉的通过反射API执行系统命令的操作。
1 | Method exec = Runtime.class.getMethod("exec", String.class); |
通过Class.getMethod
方法获取到Runtime类的exec方法之后,通过Method.invoke
并传入实例和对应参数,即完成了一次反射调用。
而MethodHandle方法句柄是如何操作的呢?(为了展示清楚,尽可能的将步骤展开成了多步)
1 | MethodType mt = MethodType.methodType(Process.class, String.class); |
可以明显的看到,原本三行的反射操作,到了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 | public class InvokeDynamicTest { |
反编译之后可以看到main函数开头的第一个函数就是invokedynamic函数
那这个invokedynamic指令究竟是调用了一个什么函数呢?
事实上,invokedynamic指令可能远不止执行一个函数那么简单。
在JVM第一次遇到该invokedynamic指令时,会去调用一个特殊的引导方法(Bootstrap Method, BSM),由引导方法初始化调用过程,返回一个CallSite实例。
如下是文档中对CallSite的定义
CallSite
是变量的持有人MethodHandle
,称为它的target
。invokedynamic
链接到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