Java虚拟机学习09 | JVM是怎么实现invokedynamic的?(下)

https://time.geekbang.org/column/article/12574

invokedynamic指令

  • invokedynamic指令是Java7引入的一条新指令,为了支持动态语言的方法调用
  • invokedynamic将调用点(CallSite)抽象成一个Java类,并且将原本由 Java 虚拟机控制的方法调用以及方法链接暴露给了应用程序
  • 在第一次执行invokenamic指令时,Java虚拟机会调用对应的启动方法(BootStrap Method)生成调用点,并且将调用点绑定到invokedynamic指令中
  • 最后Java虚拟机会直接调用绑定的调用点所连接的方法句柄
  • 在字节码中,启动方法(BootStrap Method)由方法句柄指定,它指向一个返回类型为调用点的静态方法,必须接受三个参数,分别为:Lookup实例,指代目标方法名的字符串,该调用点能够链接的方法句柄
  • Java暂时不支持直接生成invokedynamic指令,需要借助ASM工具完成

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 来实现的.

在第一次执行 invokedynamic 指令时,Java 虚拟机会调用该指令所对应的启动方法(BootStrap Method),来生成前面提到的调用点,并且将之绑定至该 invokedynamic 指令中. 在之后的运行过程中,Java 虚拟机则会直接调用绑定的调用点所链接的方法句柄. 在编译过程中,Java 编译器会对 Lambda 表达式进行解语法糖(desugar),生成一个方法来保存 Lambda 表达式的内容. 该方法的参数列表不仅包含原本 Lambda 表达式的参数,还包含它所捕获的变量.(注:方法引用,如 Horse::race,则不会生成生成额外的方法. )

根据 Lambda 表达式是否捕获其他变量,启动方法生成的适配器类以及所链接的方法句柄皆不同:

  • 如果该 Lambda 表达式没有捕获其他变量,那么可以认为它是上下文无关的.因此,启动方法将新建一个适配器类的实例,并且生成一个特殊的方法句柄,始终返回该实例,
  • 如果该 Lambda 表达式捕获了其他变量,那么每次执行该 invokedynamic 指令,我们都要更新这些捕获了的变量,以防止它们发生了变化.

为了保证Lambda表达式的线程安全,在每次invokedynamic执行时,所调用的方法句柄都需要新建一个适配器实例

Lambda与方法句柄的性能分析

即时编译能够转换Lambda表达式所使用的invokedynamic,以及对IntConsumer.accept方法的调用进行方法内联,最终优化为空操作:

  1. Lambda 表达式所使用的 invokedynamic 将绑定一个 ConstantCallSite,其链接的目标方法无法改变. 因此,即时编译器会将该目标方法直接内联进来. 对于这类没有捕获变量的 Lambda 表达式而言,目标方法只完成了一个动作,便是加载缓存的适配器类常量.
  2. accept方法在对应的字节码中只包含一个方法调用,该方法调用目标是Java编译器在Lambda语法糖解析时生成的方法,也就是Lambda表达式的内容(目标方法). 将这几个方法内联后,accept的调用也就优化成空操作

在invokedynamic 指令所执行的方法句柄能够内联,和接下来的对 accept 方法的调用也能内联的情况下,逃逸分析能够优化消除额外的新建实例开销

猜你喜欢

转载自blog.csdn.net/qq_34332035/article/details/87979853
今日推荐