本文为《深入学习 JVM 系列》第九篇文章
invokedynamic 指令
千呼万唤始出来,上一篇文章介绍了那么久的方法句柄,终于来到 invokedynamic 指令讲解了。
invokedynamic 是 Java 7 引入的一条新指令,用以支持动态语言的方法调用。具体来说,它将调用点(CallSite)抽象成一个 Java 类,并且将原本由 Java 虚拟机控制的方法调用以及方法链接暴露给了应用程序。在运行过程中,每一条 invokedynamic 指令将捆绑一个调用点,并且会调用该调用点所链接的方法句柄。
在第一次执行 invokedynamic 指令时,Java 虚拟机会调用该指令所对应的启动方法(BootStrap Method),来生成前面提到的调用点,并且将之绑定至该 invokedynamic 指令中。在之后的运行过程中,Java 虚拟机则会直接调用绑定的调用点所链接的方法句柄。
invokedynamic 的调用模式简单说就是:
- When JVM sees an invokedynamic instruction, it locates the corresponding bootstrap method in the class, and executes the bootstrap method.
- After executing the bootstrap method, a CallSite that is linked with a MethodHandle is returned;
- The invocation on the CallSite later will be transferred to real methods via a number of MethodHandles.
这里的 bootstrap 和 Methodhandle 都是用户提供的,其中 bootstrap 方法就是用户创建一个 CallSite,然后将这个 Callsite 链接到一个 MethodHandle。MethodHandle 所指向的方法可以再应用到其它的 MethodHandle, 直至最终一个方法或者多个方法。
CallSite
当 JVM 执行 invokedynamic
指令时,首先需要链接其对应的动态调用点 。在链接的时候,JVM会先调用一个启动方法(bootstrap method
)。这个启动方法的返回值是 java.lang.invoke.CallSite
类的对象。
在通过启动方法得到了 CallSite 之后,通过这个 CallSite 对象的 getTarget()
可以获取到实际要调用的目标方法句柄。 有了方法句柄之后,对这个动态调用点 的调用,实际上是代理给方法句柄来完成的。也就是说,对 invokedynamic
指令的调用实际上就等价于对方法句柄的调用,具体来说是被转换成对方法句柄的invoke方法的调用。
JDK 中提供了三种类型的动态调用点CallSite的实现:
java.lang.invoke.ConstantCallSite
、java.lang.invoke.MutableCallSite
和 java.lang.invoke.VolatileCallSite
。
ConstantCallSite
表示的调用点绑定的是一个固定的方法句柄,一旦链接之后,就无法修改。示例如下:
public class Horse { public void race() { System.out.println("Horse.race()"); } } public static void constantCallSite() throws Throwable { MethodType methodType = MethodType.methodType(void.class); MethodHandles.Lookup lookup = MethodHandles.lookup(); Horse horse = new Horse(); MethodHandle methodHandle = lookup.findVirtual(horse.getClass(), "race", methodType); ConstantCallSite callSite = new ConstantCallSite(methodHandle); MethodHandle invoker = callSite.dynamicInvoker(); invoker.invoke(horse); } 复制代码
MutableCallSite
表示的调用点则允许在运行时动态修改其目标方法句柄,即可以重新链接到新的方法句柄上。示例如下:
public class Horse { public static void say() { System.out.println("say"); } } /** * MutableCallSite 允许对其所关联的目标方法句柄通过setTarget方法来进行修改。 * 以下为 创建一个 MutableCallSite,指定了方法句柄的类型,则设置的其他方法也必须是这种类型。 */ public static void useMutableCallSite() throws Throwable { MethodType type = MethodType.methodType(void.class); MutableCallSite callSite = new MutableCallSite(type); MethodHandle invoker = callSite.dynamicInvoker(); MethodHandles.Lookup lookup = MethodHandles.lookup(); // MethodHandle horseMethodHandle = lookup.findVirtual(Horse.class, "race", type); MethodHandle horseMethodHandle = lookup.findStatic(Horse.class,"say",type); callSite.setTarget(horseMethodHandle); invoker.invoke(new Horse()); MethodHandle minHandle = lookup.findStatic(Cobra.class, "race", type); callSite.setTarget(minHandle); invoker.invoke(); } 复制代码
注意:如果使用 findVirtual 方法,得到的 MethodHandle 的 type 为 (Horse)void,与我们初始定义的 MethodType(值为()void)不一致,在 setTarget 方法中因为要对比前后两次的 type,所以会报下面这样的错误:
Exception in thread "main" java.lang.invoke.WrongMethodTypeException: MethodHandle(Horse)void should be of type ()void 复制代码
MutableCallSite.syncAll()
提供了方法来强制要求各个线程中 MutableCallSite
的使用者立即获取最新的目标方法句柄。 但这个时候也可以选择使用 VolatileCallSite
。
VolatileCallSite
作用与 MutableCallSite 类似,不同的是它适用于多线程情况,用来保证对于目标方法句柄所做的修改能够被其他线程看到。
生成 invokedynamic指令
接下来我们构建这样一段代码,其中包括启动方法 bootstrap,它将接收前面提到的三个固定参数,并且返回一个链接至 Horse.race 方法的 ConstantCallSite。
public class Circuit { public static void startRace(Object obj) { } public static void main(String[] args) { startRace(new Horse2()); } public static CallSite bootstrap(MethodHandles.Lookup l, String name, MethodType callSiteType) throws Throwable { MethodHandle mh = l.findVirtual(Horse2.class, name, MethodType.methodType(void.class)); return new ConstantCallSite(mh.asType(callSiteType)); } } class Horse2 { public void race() { System.out.println("Horse.race()"); } } 复制代码
invokedynamic 在 Java7 开始提出来,但是实际上 javac 并不支持生成 invokedynamic。接下来借助之前介绍过的字节码工具 ASM 来实现这一目的。
本次实验在 maven 项目中构建的,首先需要引入 asm 的依赖。
<dependency> <groupId>org.ow2.asm</groupId> <artifactId>asm</artifactId> <version>9.2</version> </dependency> 复制代码
然后构建一个辅助类 ASMHelper
import java.io.IOException; import java.lang.invoke.CallSite; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; import java.nio.file.Files; import java.nio.file.Paths; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Handle; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; public class ASMHelper implements Opcodes { private static class MyMethodVisitor extends MethodVisitor { private static final String BOOTSTRAP_CLASS_NAME = Circuit.class.getName().replace('.', '/'); private static final String BOOTSTRAP_METHOD_NAME = "bootstrap"; private static final String BOOTSTRAP_METHOD_DESC = MethodType .methodType(CallSite.class, MethodHandles.Lookup.class, String.class, MethodType.class) .toMethodDescriptorString(); private static final String TARGET_METHOD_NAME = "race"; private static final String TARGET_METHOD_DESC = "(Ljava/lang/Object;)V"; public final MethodVisitor mv; public MyMethodVisitor(int api, MethodVisitor mv) { super(api); this.mv = mv; } @Override public void visitCode() { mv.visitCode(); mv.visitVarInsn(ALOAD, 0); Handle h = new Handle(H_INVOKESTATIC, BOOTSTRAP_CLASS_NAME, BOOTSTRAP_METHOD_NAME, BOOTSTRAP_METHOD_DESC, false); mv.visitInvokeDynamicInsn(TARGET_METHOD_NAME, TARGET_METHOD_DESC, h); mv.visitInsn(RETURN); mv.visitMaxs(1, 1); mv.visitEnd(); } } public static void main(String[] args) throws IOException { ClassReader cr = new ClassReader("Circuit"); ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES); ClassVisitor cv = new ClassVisitor(ASM6, cw) { @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { MethodVisitor visitor = super.visitMethod(access, name, descriptor, signature, exceptions); if ("startRace".equals(name)) { return new MyMethodVisitor(ASM6, visitor); } return visitor; } }; cr.accept(cv, ClassReader.SKIP_FRAMES); Files.write(Paths.get("Circuit.class"), cw.toByteArray()); } } 复制代码
无需理解上面这段代码的具体含义(我不会,逃),然后在 terminal 端输入如下内容:
javac Circuit.java javac -cp /usr/local/apache-maven-3.8.2/repository/org/ow2/asm/asm/9.2/asm-9.2.jar:. ASMHelper.java java -cp /usr/local/apache-maven-3.8.2/repository/org/ow2/asm/asm/9.2/asm-9.2.jar:. ASMHelper java Circuit 复制代码
最后得到结果为:
Horse.race() 复制代码
如果要复现上述的过程,需要注意这样几点:
1、Circuit 和 ASMHelper 都不需要留有包名;
2、asm-xx.jar 包因为是从 maven 仓库中引入的依赖,所以就去 maven 的 repository 包获取 jar 的绝对路径。
我们最后解析查看一下 Circuit 的字节码文件,这里只截取部分:
public static void startRace(java.lang.Object); descriptor: (Ljava/lang/Object;)V flags: (0x0009) ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokedynamic #65, 0 // InvokeDynamic #0:race:(Ljava/lang/Object;)V 6: return 复制代码
到目前为止,我们已经可以通过 invokedynamic 调用 Horse2.race 方法了。为了支持调用任意类的 race 方法,
Java 8 的 Lambda 表达式
在 Java 8中,Javac 能够生成 invokedynamic 指令, 比如 lambda。
具体来说,Java 编译器利用 invokedynamic 指令来生成实现了函数式接口的适配器。这里的函数式接口指的是仅包括一个非 default 接口方法的接口,一般通过 @FunctionalInterface 注解。不过就算是没有使用该注解,Java 编译器也会将符合条件的接口辨认为函数式接口。
我们还是举个例子,来学习 lambda 表达式。
public class LambdaTest { public static void main(String[] args) { int num = 3; IntStream.of(1, 2, 3).map(i -> i * 2).map(i -> i * num).map(LambdaTest::add); } public static int add(int num) { return num + 3; } } 复制代码
上面这段代码会对 IntStream 中的元素进行三次映射。我们查看源码可知,映射方法 map 所接收的参数是 IntUnaryOperator(这是一个函数式接口)。也就是说,在运行过程中我们需要将 i->i2 、 i->ix LambdaTest::add 这三个 Lambda 表达式转化成 IntUnaryOperator 的实例。这个转化过程便是由 invokedynamic 来实现的。可以在字节码文件中寻找到踪迹。
17: invokestatic #2 // InterfaceMethod java/util/stream/IntStream.of:([I)Ljava/util/stream/IntStream; 20: invokedynamic #3, 0 // InvokeDynamic #0:applyAsInt:()Ljava/util/function/IntUnaryOperator; 25: invokeinterface #4, 2 // InterfaceMethod java/util/stream/IntStream.map:(Ljava/util/function/IntUnaryOperator;)Ljava/util/stream/IntStream; 30: iload_1 31: invokedynamic #5, 0 // InvokeDynamic #1:applyAsInt:(I)Ljava/util/function/IntUnaryOperator; 36: invokeinterface #4, 2 // InterfaceMethod java/util/stream/IntStream.map:(Ljava/util/function/IntUnaryOperator;)Ljava/util/stream/IntStream; 41: invokedynamic #6, 0 // InvokeDynamic #2:applyAsInt:()Ljava/util/function/IntUnaryOperator; 46: invokeinterface #4, 2 // InterfaceMethod java/util/stream/IntStream.map:(Ljava/util/function/IntUnaryOperator;)Ljava/util/stream/IntStream; 51: pop 复制代码
另外在编译过程中,Java 编译器会对 Lambda 表达式进行解语法糖(desugar),生成一个方法来保存 Lambda 表达式的内容。该方法的参数列表不仅包含原本 Lambda 表达式的参数,还包含它所捕获的变量。(注:方法引用,如 LambdaTest::add,则不会生成生成额外的方法。)
仔细观察可以发现,生成的方法参数列表不一致,第一个 Lambda 表达式没有捕获其他变量,而第二个 Lambda 表达式(也就是 i->i*x)则会捕获局部变量 x,所捕获的变量同样也会作为参数传入生成的方法之中。
bootstrap method
在 Oracle JDK 8 / OpenJDK 8的实现中,javac 在编译 Java源码的时候会看看一个 lambda表达式或 method reference 的目标 SAM(Single Abstract Method)类型是否是 Serializable 的,并为这个 invokedynamic 指令选择相应的 bootstrap method。
- 对普通的不可序列化SAM类型:选择 java.lang.invoke.LambdaMetafactory.metafactory() 作为bootstrap method;
- 对可序列化的SAM类型:选择
- java.lang.invoke.LambdaMetafactory.altMetafactory() 作为bootstrap method。
关于 bootstrap method 的讲解可以参考本文。
我们重点看一下这句话“ lambda表达式或 method reference 的目标 SAM(Single Abstract Method)类型是否是 Serializable 的”,经过测试发现 lambda 表达式对应的 bootstrap method 使用的是 metafactory,那么什么时候会使用 altMetafactory 呢? SAM 类型是 Serializable 又是指的是什么呢?我尝试了很多代码,都没有得到想要的结果,最终在 R大的一篇回答中找到了想要的答案,这里我只简单提及一下,详细内容推荐仔细去阅读一下。
在 R大的回答中,里面的测试代码引入了一个新的依赖 org.apache.spark
,具体是用到了里面的 VoidFunction。
@FunctionalInterface public interface VoidFunction<T> extends Serializable { void call(T var1) throws Exception; } 复制代码
对比 Java Stream 中的函数式接口,可以发现有所不同,这里只列举两个示例。
@FunctionalInterface public interface Function<T, R> @FunctionalInterface public interface IntUnaryOperator 复制代码
有意思的事情是在 org.apache.spark
中也发现了 Function 接口:
@FunctionalInterface public interface Function<T1, R> extends Serializable { R call(T1 var1) throws Exception; } 复制代码
继承了序列化接口的函数式接口,应该就是上文提到的可序列化的 SAM 类型,最终 invokedynamic 指令选择altMetafactory 为bootstrap method。
回看上文图片,可以发现,每当第一次处理 invokedynamic 时,都会调用适当的引导方法,lambda 表达式选择的是 metafactory 方法。作为 boostrap 方法执行的结果,创建了一个 CallSite 对象。根据 Lambda 表达式是否捕获其他变量,启动方法生成的适配器类以及所链接的方法句柄皆不同。
我们可以通过虚拟机参数 -Djdk.internal.lambda.dumpProxyClasses=/DUMP/PATH 导出这些具体的适配器类。
具体 JVM 参数可以设置为:
-Djdk.internal.lambda.dumpProxyClasses=/Users/xxx/IdeaProjects/java_deep_learning/DUMP/PATH 复制代码
执行代码可以得到三个 class 文件:
这三个 class 文件分别对应代码中的三个 lambda 表达式,比如说 i -> i * 2
对应 Lambda$1.class,文件内容如下:
final class LambdaTest$$Lambda$1 implements IntUnaryOperator { private LambdaTest$$Lambda$1() { } @Hidden public int applyAsInt(int var1) { return LambdaTest.lambda$main$0(var1); } } 复制代码
我们执行 javap -v -private LambdaTest\$\$Lambda\$1
命令解析 class 文件:
private com.msdn.java.hotspot.invokedynamic.LambdaTest$$Lambda$1(); descriptor: ()V flags: (0x0002) ACC_PRIVATE Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #10 // Method java/lang/Object."<init>":()V 4: return public int applyAsInt(int); descriptor: (I)I flags: (0x0001) ACC_PUBLIC Code: stack=1, locals=2, args_size=2 0: iload_1 1: invokestatic #18 // Method com/msdn/java/hotspot/invokedynamic/LambdaTest.lambda$main$0:(I)I 4: ireturn 复制代码
可以看出,如果该 Lambda 表达式没有捕获其他变量,那么可以认为它是上下文无关的。因此,启动方法将新建一个适配器类的实例,并且生成一个特殊的方法句柄,始终返回该实例。
同理我们查看一下 i -> i * num
对应的 Lambda$2.class 文件:
final class LambdaTest$$Lambda$2 implements IntUnaryOperator { private final int arg$1; private LambdaTest$$Lambda$2(int var1) { this.arg$1 = var1; } private static IntUnaryOperator get$Lambda(int var0) { return new LambdaTest$$Lambda$2(var0); } @Hidden public int applyAsInt(int var1) { return LambdaTest.lambda$main$1(this.arg$1, var1); } } 复制代码
可以看出,捕获了局部变量的 Lambda 表达式多出了一个 get$Lambda 的方法。启动方法便会所返回的调用点链接至指向该方法的方法句柄。也就是说,每次执行 invokedynamic 指令时,都会调用至这个方法中,并构造一个新的适配器类实例。
Lambda 的性能分析
通过下述代码来查看 Lambda 的性能。
//JDK9 public class LambdaPerformance { public static void target(int i) { } public static void main(String[] args) { long current = System.currentTimeMillis(); for (int i = 1; i <= 2_000_000_000; i++) { if (i % 100_000_000 == 0) { long temp = System.currentTimeMillis(); System.out.println(temp - current); current = temp; } ((IntConsumer) j -> LambdaPerformance.target(j)).accept(128);//v1 // ((IntConsumer) LambdaPerformance::target).accept(128);//v2 // LambdaPerformance.target(128); //v3 } } } 复制代码
测量结果显示,它与直接调用的性能并无太大的区别。也就是说,JDK9 的即时编译器能够将转换 Lambda 表达式所使用的 invokedynamic,以及对 IntConsumer.accept 方法的调用统统内联进来,最终优化为空操作。
这个其实不难理解:Lambda 表达式所使用的 invokedynamic 将绑定一个 ConstantCallSite,其链接的目标方法无法改变。因此,即时编译器会将该目标方法直接内联进来。对于这类没有捕获变量的 Lambda 表达式而言,目标方法只完成了一个动作,便是加载缓存的适配器类常量。
另外查看适配器类中 v1 和 v2 两种代码生成的字节码文件,可知 accept 方法中其实包含了一个方法调用,调用至 Java 编译器在解 Lambda 语法糖时生成的方法。该方法的内容便是 Lambda 表达式的内容,也就是直接调用目标方法
LambdaPerformance.target。
//v1 @Hidden public void accept(int var1) { LambdaPerformance.lambda$main$0(var1); } //v2 @Hidden public void accept(int var1) { LambdaPerformance.target(var1); } 复制代码
方法内联其实就是调用 accept 时,直接调用对应的方法。
Lambda 表达式如果捕获变量,性能又将如何呢?
public class LambdaPerformance { public static void target(int i) { } public static void main(String[] args) { int x = 2; long current = System.currentTimeMillis(); for (int i = 1; i <= 2_000_000_000; i++) { if (i % 100_000_000 == 0) { long temp = System.currentTimeMillis(); System.out.println(temp - current); current = temp; } ((IntConsumer) j -> LambdaPerformance.target(x + j)).accept(128); //v1 // LambdaPerformance.target(128 + x);//v2 } } } 复制代码
v1 和 v2 的耗时相差不大。显然,即时编译器的逃逸分析又将该新建实例给优化掉了。我们可以通过虚拟机参数 -XX:-DoEscapeAnalysis 来关闭逃逸分析。果然,这时候测得的值约为直接调用的 3 倍。如果输出 GC 日志,可以发现会频繁的触发 GC。
尽管逃逸分析能够去除这些额外的新建实例开销,但是它也不是时时奏效。它需要同时满足两个条件:invokedynamic 指令所执行的方法句柄能够内联,和接下来的对 accept 方法的调用也能内联。
比如说下面这段代码就没法内联,可以打印出内联结果,发现 target 方法没有内联。
//-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining public class LambdaPerformance { public static void target(int i) { } public static void main(String[] args) { int x = 2; for (int i = 1; i <= 20000; i++) { ((IntConsumer) j -> { Integer xx = toInteger(String.valueOf(j)); LambdaPerformance.target(x + xx); }).accept(128); } } public static Integer toInteger(String value) { return Integer.valueOf(value); } } 复制代码
只有这样,逃逸分析才能判定该适配器实例不逃逸。否则,我们会在运行过程中不停地生成适配器类实例。所以,我们应当尽量使用非捕获的 Lambda 表达式。