【JVM】字节码指令简介(四)-invokedynamic详解
方法调用和返回指令
方法调用和返回指令总共包括5个字节码指令。其中invokestatic,invokespecial调用的目标方法在编译时就已经确定,而对于invokevirtual和invokeinterface来说,调用的目标方法是在运营时才能确定。invokedynamic是动态生成的对象。
指令 | 作用 |
---|---|
invokevirtual | 用于调用对象的实例方法,根据对象的实际类型进行分派(虚方法分派),这也是Java语言中最常见的方法分派方式 |
invokeinterface | 用于调用接口方法,它会在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用 |
invokespecial | 用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法 |
invokestatic | 用于调用类方法(static方法) |
invokedynamic | 用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法,前4条调指令的分派逻辑都固化在Java虚拟机内部,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。 |
invokedynamic详解
invokedynamic指令与其他其他方法调用指令都不相同。invokedynamic则是动态的生成与给定参数类型的对象,并且放入到操作数栈上。Invokedynamic指令简化了并且提升了JVM对于动态语言的支持。
动态语言和静态语言之间的区别
静态语言对于类型的检查是在编译期,而动态语言对于类型的检查是在运行期。
invokedynamic是在Java7开始出现在java虚拟机规范中,实际上java7的javac编译器并不会生成这个字节码。到了Java8中,invokedynamic才开始使用,其中最简单的例子就是lambda表达式。我们通过如下代码来分析
public static void main(String[] args) {
Runnable runnable = () -> System.out.println("hello world");
}
编译后的字节码:
0 invokedynamic #2 <run, BootstrapMethods #0>
5 astore_1
6 return
Java只有两种值的类型:基础类型(比如int,long,float等)和对象引用,runnable显然不是基础类型,那么它只能是对象引用。那么它是哪一个对象的引用呢?我们debug一下,idea显式的对象如下图所示,它是一个名为"当前类名$lambda"的匿名内部类,显然这个匿名内部类是由Java虚拟机帮我们生成的,并不是我们自己写的。
那么这个匿名内部类是如何生成的呢?顺着字节码,我们可以发现invokedynamic指令指向了常量池中类型为CONSTANT_InvokeDynamic_info的常量,这个常量包含了动态方法调用所需的信息,它被称为引导方法(BootstrapMethod,BSM) 。则是invokedynamic实现的关键,每个invokedynamic的调用点(call site)都对应则一个BSM的常量池。这个调用点在加载时是"未连接的(unlaced)"。调用BSM后才能确定具体要调用的方法,返回的这个CallSite对象会被关联到调用点上。
BootstrapMethod介绍
上图中BootstrapMethod是一个变长属性,它存在于ClassFile的属性表中,BootstrapMethod属性记录了用于生成动态计算常量和动态调用点的信息。它的格式如下
BootstrapMethods_attribute {
u2 attribute_name_index; // 属性名称索引,它是常量池表的一个CONSTANT_Utf8_info的引用,用来表示BootstrapMethods
u4 attribute_length; // 属性长度
u2 num_bootstrap_methods; // bootstrap_methods数组中的引导方法限定符的数量
{ u2 bootstrap_method_ref; // 常量池中的引用,常量池中类型为CONSTANT_MethodHandle_info
u2 num_bootstrap_arguments; // bootstrap_methods数组的个数
u2 bootstrap_arguments[num_bootstrap_arguments]; // 每个成员都对应一个常量池表的有效索引
} bootstrap_methods[num_bootstrap_methods];
}
我们找到上面invokedynamic指令对应BootstrapMethod的bootstrap_method_ref对应常量池中的值如下,他们都引用了java.lang.invoke
包中的对象/方法,这个包中的API是Java7引入的,叫方法句柄。
<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 Handle)介绍
java.lang.invoke
包是为了支持JSR-292为动态类型支持的实现,它的主要作用是前只能依赖符号引用来确定目标方法的基础上,增加了一种动态确定目标方法的机制,也就是方法句柄MethodHandler。
- MethodHandle
MethodHandle是方法句柄类,也是这个包中最重要的类,它是一个类型化,直接可自行的底层方法。它的核心方法主要包括invokeExact
和invoke
。MethodHandle类与反射包中的Method类似。
- MethodType
MethodType表示MethodHandle接受参数或者返回类型的描述,或MethodHandle调用方传递并期望的类型和参数。MethodType必须在MethodHandle机器所有调用方正确匹配,JVM在执行invokedynamic指令, MethodHandle.invokeExact以及MethodHandle.invoke都会强制校验
- CallSite
CallSite是MethodHandle变量的持有者,链接到CallSiteinvokedynamic指令将所有调用委托给CallSite的当前目标。
下面我们通过MethodHandles调用String的replace代码如下:
public static void main(String[] args) throws Throwable {
System.out.println(replace("daddy", 'd', 'n'));
}
public static String replace(String sourceStr,char oldChar,char newChar) throws Throwable {
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType methodType = MethodType.methodType(String.class, char.class, char.class);
MethodHandle methodHandle = lookup.findVirtual(String.class, "replace", methodType);
return(String) methodHandle.invokeExact(sourceStr, oldChar, newChar);
}
main方法执行结果为,输出结果与String.replace相同
上面replace方法编译后的字节码如下,可以看到最终MethodHandle.invokeExact也是通过invokevirtual指令调用,MethodHandle.invokeExact是一个native方法,它是由Java虚拟机实现的。
0 invokestatic #6 <java/lang/invoke/MethodHandles.lookup : ()Ljava/lang/invoke/MethodHandles$Lookup;>
// 省略中间代码
19 invokestatic #10 <java/lang/invoke/MethodType.methodType : (Ljava/lang/Class;Ljava/lang/Class;[Ljava/lang/Class;)Ljava/lang/invoke/MethodType;>
// 省略中间代码
31 invokevirtual #12 <java/lang/invoke/MethodHandles$Lookup.findVirtual : (Ljava/lang/Class;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/MethodHandle;>
// 省略中间代码
41 invokevirtual #13 <java/lang/invoke/MethodHandle.invokeExact : (Ljava/lang/String;CC)Ljava/lang/String;>
44 areturn
方法句柄与反射对比
上面例子中,如果我们通过反射的方式调用String.replace,代码如下,这段代码实现的效果与通过MethodHandle调用String.replace相同。
public static String replaceByReflect(String sourceStr, char oldChar, char newChar) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
Method method = String.class.getDeclaredMethod("replace", char.class, char.class);
return (String) method.invoke(sourceStr, oldChar, newChar);
}
编译后字节码如下,它最终是通过invokevirtual指令调用了Method.invoke方法,它的底层调用的是NativeMethodAccessorImpl.invoke0方法,它也是一个本地方法。
// 省略中间代码
20 invokevirtual #14 <java/lang/Class.getDeclaredMethod : (Ljava/lang/String;[Ljava/lang/Class;)Ljava/lang/reflect/Method;>
// 省略中间代码
44 invokevirtual #17 <java/lang/reflect/Method.invoke : (Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;>
47 checkcast #7 <java/lang/String> // 返回结果类型检查
50 areturn
MethodHandle与反射Method的区别
- Reflection API的设计目的是为Java语言服务的,而MethodHandle则是为了让JVM支持动态类型,可以为所有Java虚拟机之上的所有语言服务
- Reflection与MethodHandle机制本质上都是在模拟方法调用,但是Reflection是在模拟Java代码层次的调用,而MethodHandle是在模拟字节码层次的调用
- MethodHandle中的invoke()被JVM优化,性能更好,MethodHandle在调用了findStatic()、findVirtal()、findSpecial()就已经实现了方法执行权限的检测以及类型检测,而不是调用invoke()时。这点从上面字节码就可以看出,虽然在代码中都有显示类型转换,但是MethodHandle调用invoke的代码后并没有checkcast指令,相反Method的invoke后需要调用checkcast指令。
参考资料
Java Virtual Machine Support for Non-Java Languages (oracle.com)
相关文章
转载自:https://juejin.cn/post/7202188318889508924