今天的这篇文章比较长,也比较干货,刚接触指令的读者读起来可能会有点晦涩难懂,但是希望每一位读者能够沉下心来仔细阅读这篇文章,当你完全读懂这篇文章的时候,相信你对指令和Lambda的理解会更上一层楼。
invokedynamic是Java7引入的一条新的虚拟机指令,但是Java 8才将这条指令第一次应用到lambda表达式中。
invokedynamic与invokevirtual、invokestaic、invokeinterface、invokespecial组合在一起完成了所有形式的方法调用。除了invokedynamic,其他调用指令的分派逻辑在JVM中是固定的,但是invokedynamic的分派逻辑是由用户设定的引导方法(BSM)决定的。
BSM会返回一个CallSite对象,这个对象会和invokedynamic链接在一起,再次执行这条invokedynamic也不会创建新的CallSite对象。CallSite对象是一个MethodHandle(方法句柄)的holder。方法句柄指向一个调用点真正执行的方法。下图是CallSite类的源码,可以看到在该对象中,有一个方法句柄target。
方法句柄
方法句柄代表了一个可以从invokedynamic调用点进行调用的方法。
一个方法由基本的四个内容组成:
方法句柄首先需要一个表达签名的方式,以便于查找,在Java 7以后可以通过java.lang.invoke.MethodType类来完成,它是一个不可变的类。如果想获取methodType,可以用methodType()工厂方法完成。这是一个参数可变的方法,第一个参数使用的class对象代表签名的返回类型,剩余的参数对应签名中方法参数的类型。
public void getMethodType() {
MethodType m1 = MethodType.methodType(void.class, Object.class);
System.out.println(m1.toMethodDescriptorString());
MethodType m2 = MethodType.methodType(int.class, String.class, String.class);
System.out.println(m2.toMethodDescriptorString());
}
我们来看一下上述代码的输出,如下图:
上图这段输出其实就是方法表里面的descript_index所表示的常量池中该索引处的描述字符。
现在我们有了签名,再组合方法名称以及定义方法的类来查找方法句柄。实现上述操作我们需要调用MethodHandles.lookup()方法。该方法会返回我们一个查找上下文,这个上下文会基于当前正在执行方法的访问权限查找一些特定的方法,如:findVirtual()、findConstructor()、findStatic()等。这些方法会返回实际的方法句柄。
只有在创建查找上下文的方法能够访问被请求方法的情况下,才会返回句柄。
MethodHandle中有两个方法能够触发对方法句柄的调用,invoke和invokeExtract()。这两个方法都是以接收者和调用变量作为参数。
public class ClassTest {
public void invoke() {
ClassTest receiver = new ClassTest();
// 获取toString()的签名
MethodType methodType = MethodType.methodType(String.class);
// 获取查找上下文
Lookup lookup = MethodHandles.lookup();
try {
MethodHandle toString = lookup.findVirtual(receiver.getClass(), "toString", methodType);
String result = (String) toString.invoke(receiver);
System.out.println(result);
} catch (Throwable e) {
e.printStackTrace();
}
}
@Override
public String toString() {
return "invoke success";
}
public static void main(String[] args) {
ClassTest test = new ClassTest();
test.invoke();
}
}
下面我们可以看一下上面代码的输出,如下图:
如果不能理解该段输出的话,建议再回头从文章开始阅读,希望在学习接触一个新的知识的时候每一个读者都可以做到真正理解并且可以灵活使用。
在了解了方法句柄以后,我们再来看一下方法句柄和invokedynamic指令有啥关系。
方法句柄和invokedynamic
invokedynamic通过引导方法(BSM)来使用方法句柄,与invokevirtual指令不同的是invokedynamic指令不需要receiver,它会使用BSM返回一个CallSite对象。这个对象包含一个方法句柄,代表了当前invokedynamic指令要执行的方法。
BSM方法的签名大致如下(BSM的方法名称是任意的):
static CallSite bootstrap(MethodHandles.Lookup caller, String name, MethdType type);
下面的一段代码是通过ASM字节码编织技术生成了一段打印HelloWorld的方法,其中生成的main方法中使用了invokedynamic指令。
public class ClassTest {
public static void main(String[] args) throws IOException {
final String outputClassName = "Dynamic";
try (FileOutputStream fos = new FileOutputStream("./" + outputClassName + ".class")) {
fos.write(dump(outputClassName, "bootstrap", "()V"));
}
}
public static byte[] dump(String outputClassName, String bsmName, String targetMethodDescriptor) {
final ClassWriter cw = new ClassWriter(0);
// 创建类的元数据
cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER, outputClassName, null, "java/lang/Object", null);
// 创建public无参构造器
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "", "()V", null, null);
mv.visitCode();
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "", "()V", false);
mv.visitInsn(RETURN);
mv.visitMaxs(1, 1);
mv.visitEnd();
// 创建标准的main方法
mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
mv.visitCode();
MethodType methodType = MethodType.methodType(CallSite.class, Lookup.class, String.class, MethodType.class);
Handle bootstrap = new Handle(H_INVOKESTATIC, "jvm/ClassTest", bsmName, methodType.toMethodDescriptorString());
mv.visitInvokeDynamicInsn("runDynamic", targetMethodDescriptor, bootstrap);
mv.visitInsn(RETURN);
mv.visitMaxs(0, 1);
mv.visitEnd();
cw.visitEnd();
return cw.toByteArray();
}
private static void targetMethod() {
System.out.println("Hello World!");
}
public static CallSite bootstrap(MethodHandles.Lookup caller, String name, MethodType methodType) throws NoSuchMethodException, IllegalAccessException {
Lookup lookup = MethodHandles.lookup();
Class<?> currentClass = lookup.lookupClass();
MethodType targetSignature = MethodType.methodType(void.class);
MethodHandle targetMethod = lookup.findStatic(currentClass, "targetMethod", targetSignature);
return new ConstantCallSite(targetMethod.asType(methodType));
}
}
我们通过javp指令来看一下Dynamic.class文件,如下图:
可以看出在我们的字节码指令中已经出现了invokedynamic。
Lambda表达式
public class ClassTest {
public static void main(String[] args) {
Consumer<String> consumer = System.out::println;
consumer.accept("hello world");
}
}
上面是一段简单的Lambda代码,但是他生成的字节码却是不少的,如下图:
我们来看一下main方法,在main方法中并没有直接调用System.out.println方法,而是使用了invokedynamic指令:
8: invokedynamic #4, 0 // InvokeDynamic #0:accept:(Ljava/io/PrintStream;)Ljava/util/function/Consumer;
invokedynamic指向一个类型为CONSTANT_InvokeDynamic_info的常量项#4,0是预留参数,暂时没作用
#4 = InvokeDynamic #0:#26 // #0:accept:(Ljava/io/PrintStream;)Ljava/util/function/Consumer;
#26是一个CONSTANT_NameAndType_info,表示方法名和方法签名,这个会作为参数传递给BSM。
#26 = NameAndType #41:#42 // accept:(Ljava/io/PrintStream;)Ljava/util/function/Consumer;
#0表示在Bootstrap methods中的索引。
BootstrapMethods:
0: #22 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;
Method arguments:
#23 (Ljava/lang/Object;)V
#24 invokevirtual java/io/PrintStream.println:(Ljava/lang/String;)V
#25 (Ljava/lang/String;)V
#22是一个CONSTANT_MethodHandle_info
#22 = MethodHandle #6:#37 // 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;
实际上是个方法句柄对象,这个句柄指向的就是BSM方法,在这里就是:
java.lang.invoke.LambdaMetafactory.metafactory(MethodHandles.Lookup caller, String invokedName, MethodType invokedType, MethodType samMethodType, MethodHandle implMethod, MethodType instantiatedMethodType)
BSM的前三个参数(MethodHandles.Lookup caller, String invokedName, MethodType invokedType)是固定的,后面可以附加任意数量的参数,但参数类型是有限制的,参数类型只能是以下几种:
LambdaMetafactory.metafactory多带了三个参数,在我们的示例中如下:
Method arguments:
#23 (Ljava/lang/Object;)V
#24 invokevirtual java/io/PrintStream.println:(Ljava/lang/String;)V
#25 (Ljava/lang/String;)V
到这里关于Lambda的编译阶段就已经完成了,我们再看看运行时invokedynamic指令是如何运行的。
invokedynamic运行时
每一个invokedynamic指令都称为Dynamic Call Site(动态调用点),invokedynamic的执行大概需要两步。
1. 获取CallSite(调用点对象)
CallSite是由BSM返回的,所以这一步就是调用BSM方法,调用BSM方法可以看做invokevirtual指令执行一个invoke方法,如下:
// 前四个三处是固定的,被依次压入操作数栈
// MethodHandler,这个方法句柄指向BSM(这里就是LambdaMetafactory.metafactory)
// Lookup,调用者,是invokedynamic指令所在类的上下文
// name,lambda所实现的方法名,这里是accept
// MethodType, 调用点的方法签名,这里是methodType(Consumer.class, PrintStream.class)
invoke(MethodHandle, Lookup, String, MethodType, /*其他附加参数*/) CallSite
接下来就是执行LambdaMetafactory.metafactory的方法,该方法会创建一个匿名类(通过ASM编织字节码在内存中生成),然后通过Unsafe直接加载不会写到文件里。可以通过下面的参数让JVM运行的时候输出到文件。
-Djdk.internal.lambda.dumpProxyClasses=<path>
我们看一下生成的匿名类:
package jvm;
import java.io.PrintStream;
import java.lang.invoke.LambdaForm.Hidden;
import java.util.function.Consumer;
// $FF: synthetic class
final class ClassTest$$Lambda$1 implements Consumer {
private final PrintStream arg$1;
private ClassTest$$Lambda$1(PrintStream var1) {
this.arg$1 = var1;
}
private static Consumer get$Lambda(PrintStream var0) {
return new ClassTest$$Lambda$1(var0);
}
@Hidden
public void accept(Object var1) {
this.arg$1.println((String)var1);
}
}
最后就是创建一个CallSite,绑定一个MethodHandle到target,指向的方法就是生成的类中的静态方法ClassTest$$LambdaLambda(PrintStream var0)Consumer,然后把调用点对象(也就是CallSite)返回,到这里BSM方法执行完毕。
关于匿名类的生成及CallSite的构建搭建可以自己去看一下源码,这里就不展开了。
2. 执行方法句柄
执行方法句柄的过程其实就像普通的方法调用,此时操作数栈顶的元素是CallSite对象(实际上是方法句柄),如果是ConstantCallSite的时候,invokedynamic会直接跟他的方法句柄链接。
传入PrinStream对象,执行方法,返回一个Consumer对象压入栈顶,到这里invokedynamic指令已经执行完成。
下面我们再分一下从invokedynamic开始的指令,后面的就比较简单了。
8:这里就返回一个Consumer对象压入栈顶
13:将栈顶的Consumer存储到局部变量表的第2个Slot槽中
14:将第2个Slot槽中的Consumer对象压入栈顶
15:将常量hello world压入操作数栈顶
17:调用Consumer的accept方法,也就是ClassTest$Lambda1类中的accpet方法,实际上是打印了hello world
剩下的就是重复使用这个Consumer对象(局部变量表的第2个Slot槽中将其加载到操作数栈中),然后调用accpet方法进行打印。
本期invokedynamic指令和Lambda就介绍到这,我们下期再见!!!