【07】JVM是怎么实现invokedynamic的

在Java中,方法调用会被编译为invokeStaticinvokeSpecialinvokVirtual以及invokeInterface四种指令。这些指令与包含目标方法类名、方法名以及方法描述符的符号引用捆绑,在实际运行之前,JVM根据这个符号引用链接到具体目标方法。

JDK7 引入新的指令invodeDynamic该指令的调用机制抽象出调用点这一个概念,并允许应用程序将调用点链接至任意符合条件的方法上。同时JDK7 还配套引入了更加低层、更加抽象的方法抽象:方法句柄(invokedynamic 底层机制的基石:方法句柄。)。

一、方法句柄

1.方法句柄概念

强类型,能够被直接执行的引用。
方法句柄的类型是由所指向方法的参数以及返回类型组成的。它是用来确认方法句柄是否适配的唯一关键。

  • 方法句柄的获取
class Foo {
  private static void bar(Object o) {
    ..
  }
  public static Lookup lookup() {
    return MethodHandles.lookup();
  }
}

// 获取方法句柄的不同方式
MethodHandles.Lookup l = Foo.lookup(); // 具备 Foo 类的访问权限
Method m = Foo.class.getDeclaredMethod("bar", Object.class);
MethodHandle mh0 = l.unreflect(m);

MethodType t = MethodType.methodType(void.class, Object.class);
MethodHandle mh1 = l.findStatic(Foo.class, "bar", t);

  • 方法句柄的权限
    与反射 API 不同,其权限检查是在句柄的创建阶段完成的。在实际调用过程中,JVM不会检查方法句柄的权限。

方法句柄的访问权限不取决于方法句柄的创建位置,而是取决于 Lookup对象的创建位置

举个例子,对于一个私有字段,如果 Lookup 对象是在私有字段所在类中获取的,则这个Lookup对象便拥有对该私有字段的访问权限,
即使是在所在类的外边,也能够通过该 Lookup 对象创建该私有字段的getter 或 setter

2.方法句柄的操作

  1. 方法句柄的调用有两种模式:
  • invokeExact(需要严苛匹配参数类型)
    一个方法句柄将接收一个 Object 类型的参数,如果你直接传入String作为实际参数,则方法句柄的调用会在运行时抛出方法类型不匹配的异常
  • invoke(自动适配参数类型)
    invoke 会调用 MethodHandle.asType方法,生成一个适配器方法句柄,对传入的参数进行适配,再调用原方法句柄;调用原方法句柄的返回值同样会先进行适配,然后再返回给调用者。
  1. 方法句柄支持增删改参数的操作
  • 改操作:MethodHandle.asType 方法
  • 删操作:将传入的部分参数就地抛弃,再调用另一个方法句柄。它对应的API 是 MethodHandles.dropArguments方法
  • 增操作:它会往传入的参数中插入额外的参数,再调用另一个方法句柄,它对应的 API 是 MethodHandle.bindTo 方法;Java 8 中捕获类型的 Lambda 表达式便是用这种操作来实现的
增操作还可以用来实现方法的柯里化。举个例子,有一个指向 f(x, y) 的方法句柄,我们可以通过将 x 绑定为 4,生成另一个方法句柄 g(y) = f(4, y)。
在执行过程中,每当调用 g(y) 的方法句柄,它会在参数列表最前面插入一个 4,再调用指向 f(x, y) 的方法句柄。

柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术

3.方法句柄的实现

方法句柄的调用和反射调用一样,都是间接调用。因此,它也会面临无法内联的问题。不过,与反射调用不同的是,方法句柄的内联瓶颈在于即时编译器能否将该方法句柄识别为常量。

二、invokeDynamic指令

1.调用点介绍

invokedynamic 是 Java 7 引入的一条新指令,用以支持动态语言的方法调用。具体来说,它将调用点(CallSite)抽象成一个 Java 类,并且将原本由 Java 虚拟机控制的方法调用以及方法链接暴露给了应用程序。在运行过程中,每一条 invokedynamic 指令将捆绑一个调用点,并且会调用该调用点所链接的方法句柄。
在第一次执行 invokedynamic 指令时,Java 虚拟机会调用该指令所对应的启动方法(BootStrap Method),来生成前面提到的调用点,并且将之绑定至该 invokedynamic 指令中。在之后的运行过程中,Java 虚拟机则会直接调用绑定的调用点所链接的方法句柄。

invokedynamic 的目的,就是将调用点与目标方法的链接交由应用程序来做,并且依赖于应用程序对目标方法进行验证。所以,如果应用程序将赛跑方法链接至兔子的睡觉方法,那也只能怪应用程序自己了。

2.Java8 中lambda表达式

Java8中的lambda是借助于invokeDynamic来实现的。

具体来说,Java 编译器利用 invokedynamic 指令来生成实现了函数式接口的适配器。

函数式接口指的是仅包括一个非 default 接口方法的接口,一般通过 @FunctionalInterface 注解,不过就算是没有使用该注解,Java 编译器也会将符合条件的接口辨认为函数式接口

int x = ..
IntStream.of(1, 2, 3).map(i -> i * 2).map(i -> i * x);

上面这段代码会对 IntStream 中的元素进行两次映射。映射方法 map 所接收的参数是 IntUnaryOperator(这是一个函数式接口)。
也就是说,在运行过程中我们需要将 i->i*2 和 i -> i*x 这两个lambda表达式转化成IntUnaryOperator实例,
这个转换过程就是通过invokeDynamic实现的;

在编译过程中,Java 编译器会对 Lambda 表达式进行解语法糖(desugar),
生成一个方法来保存 Lambda 表达式的内容。该方法的参数列表不仅包含原本 Lambda 表达式的参数,还包含它所捕获的变量。
(注:方法引用,如 Horse::race,则不会生成生成额外的方法。)

在上面那个例子中,第一个 Lambda 表达式没有捕获其他变量,而第二个 Lambda 表达式(也就是 i->i*x)则会捕获局部变量 x。
这两个 Lambda 表达式对应的方法如下所示。可以看到,所捕获的变量同样也会作为参数传入生成的方法之中。
  • 13
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值