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})
通过上面创建的lookup
和methodType
,可以用来定位 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 = <invokedynamic(this, j)>;
}
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
网友评论