「JVM 执行引擎」动态类型语言支持

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 虚拟机》
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Aurelius-Shu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值