概述
jvm
字节码中有有五种Invoke指令,其中4种功能和使用都非常好理解:
- invokestatic静态方法调用
- invokeinterface接口方法调用
- invokevirtual正常方法调用
- invokespecial特殊方法如private方法、构造方法调用
但是invokedynamic
的作用和这四种完全不同,他更加复杂,更加黑盒。
描述一下invokedynamic
的作用是,调用一个指定返回值为CallSite
的Bootstrap
方法,然后拿出CallSite
中绑定的MethodHandle
方法句柄,最后运行这个方法得到返回值,所以一行invokedynamic
指令转换成伪代码是分别运行了三个函数如下:
bootstrapmethod(xxx).getTarget().invokeExact(xx);
看完会一头雾水,为什么要转换成这样三个函数调用,和直接生成这三个函数调用的字节码有区别吗,等等问题,本文我们来慢慢介绍。
MethodHandle
我们先来解释下MethodHandle
方法句柄,他与用反射拿到的Method
类似,两者都是方法的载体,并且也都能通过自身的invoke/invokeExact(xx)
对方法进行调用,但是有一些重要的区别:
MethodHandle
是编译器进行类型安全检查,并且只在创建的时候进行访问权限检查。Method
是运行时进行类型安全检查,并且在每次invoke
的时候进行访问权限检查。MethodHandle
是更容易被JIT
优化。Method
每次使用都有开销。
大概就是MethodHandle
使用更复杂一些,且不含有方法丰富的元数据信息,但是性能更好;Method
则反之。反射比较简单,通过Class
的getMethod/getDeclaredMethod
可以获取,而MethodHandle
需要通过Lookup
进行方法查找获取。在官方的blog中也介绍了MethodHandle
为什么要引入和对比反射Method
的区别。
// 反射获取Method,并调用
Method m = String.class.getMethod("toString");
m.invoke("hi");
// MethodHandle需要用lookup查找(lookup是当前创建的,就无法找到当前看不到的方法,比如其他类的private方法,相比反射的后门写法更安全)
MethodHandles.Lookup lookup = MethodHandles.lookup();
// 查找String类中,名为toString的,入参为空返回值为String的方法
MethodHandle mh = lookup.findVirtual(String.class, "toString", MethodType.methodType(String.class));
mh.invoke("hi");
// or
mh.invokeExat("hi");
MethodHandle
中invoke/invokeExact
区别是后者的类型是静态的性能好,前者可以是动态的,动态场景可以使用,当前场景是静态的,只能是String
类型。
findVirtaul
是查找virtaul
方法与上面提到的invokevirtual
类似的查找虚方法(含接口方法),其他方法可以看下对应的其他findXXX
。
提出疑问
我们回看invokedynamic
伪代码,最后就是调用了MethodHandle#invokeExact
方法,那换句话说,其实invokedynamic
就是查找一个方法并调用他。那为什么还需要BootstrapMethod(BSM)
和CallSite
这两层嵌套呢?
这是因为BSM
这个方法比较特殊,在多次invokedynamic
中同一个BSM
的返回值会在第一次调用后缓存,后续调用会直接读取缓存中的CallSite
对象返回,BSM
的描述信息会被单独记录到字节码的属性中,用来作为缓存的key的一部分,BSM
有这个特殊的缓存机制,所以是必要的一层,那CallSite
呢?
(下面MH
作为MethodHandle
的缩写,BSM
作为BootstrapMethod
缩写)
CallSite
上面的方法句柄是绑定到CallSite
中的,CallSite
是个抽象类,他有多种实现,常见的有三种ConstantCallSite
静态调用点,即绑定的MethodHandle
是固定的不会改变的;MutableCallSite
是绑定的MH
可以被修改,但是多线程需要手动同步,适合单线程可变场景;VolatileCallSite
则是线程安全的可变场景。
回到上面的问题,为什么需要CallSite
这一层,如果只有静态的调用点ConstansCallSite
,那确实不需要CallSite
这一层封装,但是考虑到绑定的MH
可以修改的情况,而CallSite
又是被BSM
缓存的对象,缓存是不变的,对应的方法想要修改就只能用BSM
-MutableCallSite/VolatileCallSite
-MethodHandle
这样三层结构了,这样只需要修改MutableCallSite
中指向的MH
即可改变缓存的实质内容咯。
BootstrapMethod (BSM)
上面已经提到了BSM
在invokedynamic
中的特殊的缓存机制,这里一定要注意,如果自己用java
代码手动调用BSM
方法是不会触发这个缓存机制的,只有通过invokedynamic
指令调用的BSM
才会有缓存和优化,上面提到BSM
的返回值必须是CallSite
类型,那么这个函数的入参呢?
BSM
的入参也有着严格的规范,他的前三个参数分别是:
MethodHandles.Lookup
:查找MH
要用到的lookup
String
:MH
指向的方法的名字MethodType
:MH
指向的方法的返回值入参描述。
这三个参数由JVM
自动注入,不需要额外指定。后面参数是变长参数Object...
,如果自己写BSM
的话,需要特殊的参数可以放到最后,自己来定义逻辑。
lambda
这里我们以java
中最常出现invokedynamic
的lambda
表达式为例,来看下是如何工作的。
这段使用了lambda的代码
String hi = "hi";
Runnable r = () -> {
System.out.println(hi);
};
产生的字节码是这样的
ALOAD 1 // 加载hi到栈顶
INVOKEDYNAMIC run(Ljava/lang/String;)Ljava/lang/Runnable; [
// handle kind 0x6 : INVOKESTATIC
java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
// arguments:
()V,
// handle kind 0x6 : INVOKESTATIC
com/example/LambdaTest.lambda$main$0(Ljava/lang/String;)V,
()V
]
这里的BSM
是java/lang/invoke/LambdaMetafactory.metafactory
这个方法,他的入参是Lookup
String
MethodType
(前三个是固定的,前面提到过),然后是MethodType
MethodHandle
MethodType
三个参数如下图,前三个都是vm
自动注入的,后面三个参数就分别对应上面字节码// arguments
下面的三行会分别对应这三个参数。
在注释中有对后面三个参数的解释:samMethodType
是lambda
要实现的接口中的方法的签名,我们这里是void run()
所以签名是()V
;implMethod
则是最终实现这个run
方法的MH
句柄,这里可以看到是LambdaTest.lambda$main$0
当前类中的一个合成方法,这个方法是编译器自动生成的;最后instantiatedMethodType
这里先认为和samMethodType
要保持一致。
上面提到的当前类中的合成方法,在字节码中可以看到,他的内容如下,是一个入参String
空返回值的函数,内容是打印入参的String
。
我们继续看BSM
做的事情,上来创建一个InnerClassLambdaMetafactory
对象,主要是对一些字段赋值,并且这里用到了ASM
库的ClassWriter
准备在内存中创建一个匿名类的字节码。
这个匿名类还没有完成,目前是有了类名、构造方法、参数的信息,但是还没有真正开始构建字节码,最终如下图其实是在spinInnerClass
中完成了这个匿名了类的构建,并加载到当前jvm,然后CallSite
的MH
指向了匿名类get$Lambda
函数(这是有捕捉上下文的情况),没有的话走if
分支直接指向匿名类的空参数构造方法。
spinInnerClass
的内容如下,可以不用逐行看,直接到最后,把生成类的字节码打个断点,写到一个文件中。
查看文件反编译结果,是一个实现了Runnable
接口的类,并且构造方法有一个String
入参,这个在run
方法中会用到来打印,get$Lambda
方法也看到了,是个静态方法,返回的就是new
了一个当前类,把String
传入了。
好了,最后理一下思路,lambda
的工作原理:
- 第一步,编译器把
lambda
函数构造一个当前类中的合成方法,如果有上下文捕捉变量则作为函数的入参,如果没有捕捉this
则当前方法是static
否则是非static
。 - 第二步,
invokedynamic
指令调用的BSM
是会在内存中临时创建一个实现了Runnable
接口的匿名内部类,并且如果有上下文变量捕捉,该类的构造方法会依次放置这些变量,将其设置为字段。然后run
方法中直接调用合成方法,合成方法如果需要捕捉的上下文变量,则直接通过this.xx
即可传入;最后产生一个get$lambda
的静态方法创建一个当前类的对象。 - 第三步,
BSM
函数的返回值是ConstantCallSite
他的MH
指向匿名内部类的get$lambda
方法,这是一个返回一个匿名内部类的实例的方法。 - 第四步,
BSM(xxx)
返回了CallSite
,然后调用CallSite.target()
获取到这个get$lambda
的MH
,调用MH.invokeExact(xxx)
,入参xxx是栈顶的hi
字符串,最后返回一个实现了Runnable
的匿名类实例,并且接下来会赋值给变量r
。 - 第五步,当第二次调用该指令的时候,不再走
BSM
方法体,而是jvm缓存中拿到第一次执行后的CallSite
避免再次生成一个匿名内部类。
如何验证第二次没有走BSM
呢?在bsm java.lang.invoke.LambdaMetafactory#metafactory
第一行打断点,运行下面函数,只有第一次运行会命中,第二次走了缓存。
....
public static void main(String... args) {
// 两次运行,
new LambdaTest().test();
new LambdaTest().test();
}
public void test() {
String hi = "hi";
Runnable r = () -> {
System.out.println(hi);
};
r.run();
}
...
最后一张图来总结Runnable
用lambda
和匿名类写法,创建和运行的过程。
思考:invokedynamic
相比直接用匿名类实现lambda
有什么优势呢?
invokedynamic
也是一个匿名类,只不过是内存中的匿名类,不需要编译期生成class文件,避免了大量零碎的class文件;在性能上,MH
可以被jvm
的jit
更好的内联增强,但是匿名类也会被jit
增强,所以性能上没有优势,甚至因为forward
一层转到调用合成方法,可能还要更慢一点。
动态语言
indy
的介绍一般都会提及在lambda
字符串拼接(jdk9+)
动态语言
支持上会被使用。那我们就来看一下动态语言支持上是如何使用的。以groovy
为例,在groovy
中是可以不指定类型的,变量是动态类型的。并且可以在对象上动态追加新的属性。
def obj = new Object()
// 类级别的MetaClass
Object.metaClass.getA = { -> "1" }
println obj.a // 1
// 实例级别的MetaClass
obj.metaClass.getA = { -> "2" }
println obj.a // 2
// 实例级别的MetaClass修改
obj.metaClass.getA = { -> "3" }
println obj.a // 3
def
是不指定类型的动态类型,即obj当前是A
类型,后面还可以赋值为B
类型,动态类型是通过直接声明为Object
来实现的:
def x = 1; // Object x = 1;
x = "str"; // x = "str";
而动态属性追加和动态属性的解析是groovy
的核心功能,这个功能的实现invokedynamic
并不是必须的,我们先来说不用invokedynamic
的实现思路。
Groovy
中有三种对象:java对象、groovy对象和实现了GroovyInterceptable
接口的对象。
对于POJO
java对象来说,对于类和对象实例都有全局注册的Map存储了对应类/实例所对应一个扩展属性用的MetaClass
对象,这个对象我们可以简单理解成一个Map
来存储新增的属性的k和v。
当调用obj.a
的时候,会分别到对象的MetaClass
->类的MetaClass
->POJO
对象本身中寻找有没有getA
方法或者a
属性,所以上面代码第一次打印,只有类的MetaClass
中含有getA所以打印1,第二次运行的时候实例自身的MetaClass
中有getA
方法了,所以直接走实例的扩展MetaClass
。
这个方案为已有的java类和对象,提供了很高的灵活性,可以动态增删属性,但是对于对象的MetaClass
和对象本身的销毁需要额外的考虑,来避免内存长期占用。
对于第二种情况,在groovy
中声明的class
创建的GroovyObject
就更简单一些了,groovy
中的所有class
在生成字节码的时候,会自动增强,来实现一个GroovyObject
接口,这个接口中就需要有MetaClass
的getset方法,直接在对象中关联MetaClass
就不用全局的注册的Map
了,不过最终效果是类似的,如下图。详细的元编程,可以看官方文档主要是运行时元编程部分。
我们就不再展开了,因为有一些扯远了,我们回到invokedynamic
,对于obj.a
这行代码会从groovy
生成的脚本的字节码能看到invokedynamic
指令。也就是groovy
中最基础的.
运算符都是用invokedynamic
指令实现的,他的实现逻辑是通俗介绍:初次运行BSM
,会将上面我们介绍的属性/方法层层查找
的逻辑,找到之后将方法的MH
设置到CallSite
中缓存起来。第二次调用的时候,就可以直接用缓存中的MH
,省去了查找的逻辑,这就是invokedynamic
的对动态语言的效率增强的核心逻辑,当然这是一个通俗介绍,实际代码非常的复杂,考虑的情况也非常多。
验证缓存的过程,给出这样一段groovy
脚本
class A {
def name = "A"
}
def function = { x -> x.name}
function(new A())
function(new A())
配置启动参数-Dgroovy.indy.logging=true
运行后,会打印动态的方法搜索的过程,x.name
这里会搜索name
的get
方法,function
运行2次,第一次走BSM
会进行name的get
搜索,第二次走缓存,不再搜索,所以只会搜到一次name
的搜索。