invokedynamic
指令实现动态类型语言(Dynamically Typed Language)支持(JDK 7 新增指令,JDK 8 用以实现 Lambda 表达式);
1. 动态类型语言
动态类型语言
的关键特征是它的类型检查的主体过程是在运行期而不是编译期;变量无类型,变量的值才有类型
;编译期就进行类型检查过程的语言(如 C++、Java 等)就是静态类型语言;
Java 语言在编译期生成方法完整的符号引用,并作为方法调用的参数存储在 Class 文件;符号引用包含了方法定义在那个具体类型、方法的名字、参数顺序、参数类型和方法返回值信息,JVM 可以通过这个符号引用翻译出该方法的直接引用;
JavaScript 等动态类型语言编译时则只能确定方法名称、参数、返回值等信息,而无法确认方法所在的位置(方法接收者不固定);变量没有类型,变量值才有类型;
静态类型语言能在编译期确认变量类型,编译期可以更全面严谨的检查类型,让潜在问题尽早暴露,利于大规模项目的稳定;
动态类型语言则极具灵活性,可以简洁清晰的视线静态类型语言需要大量代码实现的功能,可以较好的提升开发效率;
2. Java 动态类型
JVM 在适当扩展后可以支持其他语言运行,能够在同一 JVM 实现静态类型语言与动态类型语言;
JVM 前 4 个方法调用指令(invokevirtual、invokespecial、invokestatic、invokeinterface)的第一个参数都是被调用方法的符号引用(CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info 常量),而方法的符号引用会在编译期产生,但动态类型语言只能在运行期确认方法的接收者;
若采用占位符类型实现动态类型,会增加其实现复杂度,并带来额外的性能(方法内联无法进行)和内存开销(内联缓存压力大);因此 JDK 7 新增 invokedynamic 指令及 java.lang.invoke 包来从 JVM 层面实现动态类型;
3. java.lang.invoke 包
java.lang.invoke
包在单纯依赖符号引用确认调用目标方法之外,提供了一种新的动态确认目标方法的机制,即方法句柄
(Method Handle
,类似 C/C++ 的函数指针,Function Pointer,C# 里的委派,Delegate);
Java 语言无法单独把一个函数作为另一个函数的参数传递,需要设计一个带同样参数和返回值的方法的接口,然后以实现这个接口的对象作为参数;
public class MethodHandleTest {
static class ClassA {
public void println(String s) {
System.out.println(s);
}
}
public static void main(String[] args) throws Throwable {
Object obj = System.currentTimeMillis() % 2 == 0 ? System.out : new ClassA();
// 无论 obj 最终是哪个实现类,下面这句都能正确调用到 println 方法。
getPrintlnMH(obj).invokeExact("abc");
// 输出:
// abc
}
private static MethodHandle getPrintlnMH(Object reveiver) throws Throwable {
// MethodType: 代表“方法类型”,包含了方法的返回值(methodType()的第一个参数)和具体参数(methodType()第二个及以后的参数)。
MethodType mt = MethodType.methodType(void.class, String.class);
// lookup() 方法来自于 MethodHandles.lookup,这句的作用是在指定类中查找符合给定的方法名称、方法类型,并且符合调用权限的方法句柄。
// 因为这里调用的是一个虚方法,按照Java语言的规则,方法第一个参数是隐式的,代表该方法的接 收者,也即this指向的对象,
// 这个参数以前是放在参数列表中进行传递,现在提供了bindTo() 方法来完成这件事情。
return MethodHandles.lookup().findVirtual(reveiver.getClass(), "println", mt).bindTo(reveiver);
}
}
getPrintlnMH
模拟了 invokevirtual 指令的执行过程,只是它的分派逻辑是由用户的 Java 方法实现的,其中 MethodHandle 对象可以当做是最终调用方法的一个引用;
MethodHandle vs. Reflection
- Reflection 和 MethodHandle 本质上都是在模拟方法调用,但 Reflection 是在模拟 Java 代码层面的方法调用,而 MethodHandle 是在模拟字节码层面的方法调用(MethodHandle.Lookup 的 3 个方法 findStatic()、findVirtual()、findSpecial() 正是对应了 invokestatic、invokevirtual + invokeinterface、invokespecial 指令的行为,这些底层细节 Reflection API 是不需要关心的);
- Relection 中的 java.lang.reflect.Method 对象(Java 端的全面映像,如方法签名、描述符、方法属性表的 Java 端表示、执行权限等运行期信息)比 MethodHandle 中的 java.lang.MethodHandle 对象(仅执行该方法的相关信息)包含的信息多,Reflection 是重量级,MethodHandle 是轻量级;
- MethodHandle 是模拟字节码的方法指令调用,理论上也可以模拟支持如
方法内联
等 JVM 在这方面的优化;Reflection 则几乎不可能直接实现各类调用点优化; - MethodHandle 可以服务于 JVM 上云霄的任何语言,而 Reflection API 只为 Java 语言服务;
4. invokedynamic 指令
invokedynamic
指令与 MethodHandle
机制的作用类似,解除了 JVM 原有 4 条 invoke 指令的分派完全固化在 VM 内部的问题,可以将查找目标方法的决定权从 JVM 转交给用户代码;一个用字节码和 Class 文件中的数量、常量来实现,一个用上层代码和 API 来实现;
invokedynamic
指令的位置被称作动态调用点
(Dynamically Computed Call Site
),指令的第一个参数是 CONSTANT_InvokeDynamic_info
常量(表达了引导方法、方法类型、方法名称;这是 JDK 7 新加入的常量;不再是代表方法符号引用的 CONSTANT_Methodref_info
常量),跟进该常量中的信息找到并执行引导方法,从而获得一个 CallSite 对象,最终调用到哟啊执行的目标方法上;
引导方法
,Bootstrap Method,参数固定,返回值是 java.lang.invoke.CallSite
对象,代表真正要被执行的方法调用;存放在新增的 BootstrapMethods 属性中;
InvokeDynamic 指令演示
public class InvokeDynamicTest {
public static void main(String[] args) throws Throwable {
INDY_BootstrapMethod().invokeExact("abc");
// 输出:
// hello String:abc
}
public static void testMethod(String s) {
System.out.println("hello String:" + s);
}
public static CallSite BootstrapMethod(MethodHandles.Lookup lookup, String name, MethodType mt) throws NoSuchMethodException, IllegalAccessException {
return new ConstantCallSite(lookup.findStatic(InvokeDynamicTest.class, name, mt));
}
private static MethodType MT_BootstrapMethod() {
return MethodType.fromMethodDescriptorString("(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;",null);
}
private static MethodHandle MH_BootstrapMethod() throws Throwable {
return MethodHandles.lookup().findStatic(InvokeDynamicTest.class, "BootstrapMethod", MT_BootstrapMethod());
}
private static MethodHandle INDY_BootstrapMethod() throws Throwable {
CallSite cs = (CallSite) MH_BootstrapMethod().invokeWithArguments(MethodHandles.lookup(), "testMethod",
MethodType.fromMethodDescriptorString("(Ljava/lang/String;)V", null));
return cs.dynamicInvoker();
}
}
查看使用 invokedynamic 的字节码需借助 INDY 工具进行反编译;
5. 方法分派规则
我们掌握方法分派规则后,可以做些什么以前做不到的事情?
方法调用问题
class GrandFather {
void thinking() {
System.out.println("i am grandfather");
}
}
class Father extends GrandFather {
void thinking() {
System.out.println("i am father");
}
}
class Son extends Father {
void thinking() {
// 如何在这里调用 grandfather 的 thinking()?
}
}
在 JDK 7 引入 invokedynamic 和 java.lang.invoke 之前,无法通过纯粹 Java 语言实现在 Son 中调用 GrandFather 的 thinking()(使用 ASM 等字节码工具可以通过生成字节码达成这个目的);Son 的 thinking() 中无法获取到 GrandFather 的对象引用;
JDK 7 Update 9 的实现
class Son extends Father {
void thinking() {
try {
MethodType mt = MethodType.methodType(void.class);
MethodHandle mh = lookup().findSpecial(GrandFather.class, "thinking", mt, getClass());
mh.invoke(this);
} catch (Throwable e) { }
}
}
// 输出:
// 在 JDK 7 Update 9 之前
// i am grandfather
// 在 JDK 7 Update 10 之后
// i am father
该实现在 JDK 7 Update 9 之后被视为一个安全缺陷呗修正(必须保证 findSpecial() 查找方法时受到的访问约束应与使用 invokespecial
指引一样,只能访问到其父类中的方法);
通过反射绕开访问保护
访问保护是通过一个 allowedModes
的参数来控制,这个参数可以被设置成 TRUSTED
从而绕开所有的保护措施;这个参数只在 Java 类库中使用,不开放给外部,但通过反射可以打破限制;
void thinking() {
try {
MethodType mt = MethodType.methodType(void.class);
Field lookupImpl = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
lookupImpl.setAccessible(true);
MethodHandle mh = ((MethodHandles.Lookup) lookupImpl.get(null))
.findSpecial(GrandFather.class, "thinking", mt, GrandFather.class);
mh.invoke(this);
} catch (Throwable e) {
}
}
// 输出:
// 当前所有 JDK 版本
// i am grandfather
上一篇:「JVM 执行引擎」方法调用原理
下一篇:「JVM 执行引擎」栈架构的字节码的解释执行引擎
PS:感谢每一位志同道合者的阅读,欢迎关注、评论、赞!
参考资料:
- [1]《深入理解 Java 虚拟机》