MethodHandle与反射Method区别,invokedynamic指令

MethodHandle与反射Method区别

MethodHandle的使用方法和效果上与Reflection都有众多相似之处。不过,它们也有以下这些区别:
  1. Reflection和MethodHandle机制本质上都是在模拟方法调用,但是Reflection是在模拟Java代码层次的方法调用,而MethodHandle是在模拟字节码层次的方法调用。在MethodHandles.Lookup上的三个方法findStatic()、findVirtual()、findSpecial()正是为了对应于invokestatic、invokevirtual & invokeinterface和invokespecial这几条字节码指令的执行权限校验行为,而这些底层细节在使用Reflection API时是不需要关心的。
  2. Reflection中的java.lang.reflect.Method对象远比MethodHandle机制中的java.lang.invoke.MethodHandle对象所包含的信息来得多。前者是方法在Java一端的全面映像,包含了方法的签名、描述符以及方法属性表中各种属性的Java端表示方式,还包含有执行权限等的运行期信息。而后者仅仅包含着与执行该方法相关的信息。用开发人员通俗的话来讲,Reflection是重量级,而MethodHandle是轻量级。
  3. 由于MethodHandle是对字节码的方法指令调用的模拟,那理论上虚拟机在这方面做的各种优化(如方法内联),在MethodHandle上也应当可以采用类似思路去支持(但目前实现还不完善)。而通过反射去调用方法则不行。
  4. 和反射相比好处是:

    调用 invoke() 已经被JVM优化,类似直接调用一样。
    性能好得多,类似标准的方法调用。
    当我们创建MethodHandle 对象时,实现方法执行权限检测(如2中的MethodHandles.Lookup上的三个方法findStatic()、findVirtual()、findSpecial()),而不是调用invoke() 时。

MethodHandle与Reflection除了上面列举的区别外,最关键的一点还在于去掉前面讨论施加的前提“仅站在Java语言的角度看”之后:Reflection API的设计目标是只为Java语言服务的,而MethodHandle则设计为可服务于所有Java虚拟机之上的语言,其中也包括了Java语言而已。

invokedynamic指令

invokedynamic指令需要与MethodHandle方法句柄结合起来使用。该指令的灵活性在很大程度上取决于方法句柄的灵活性。对于invokedynamic指令来说,在Java源代码中是没有直接的对应产生方式的。这也是invokedynamic指令的新颖之处。它是一个完全的Java字节代码规范中的指令。传统的Java编译器并不会帮开发人员生成invokedynamic指令。为了利用invokedynamic指令,需要开发人员自己来生成包含这个指令的Java字节代码。因为这个指令本来就是设计给动态语言的编译器使用的,所以这种限制也是合理的。对于一般的程序来说,如果希望使用这个指令,就需要使用操作Java字节代码的工具来完成。

在字节代码中每个出现的invokedynamic指令都成为一个动态调用点(dynamic call site)。每个动态调用点在初始化的时候,都处于未链接的状态。在这个时候,这个动态调用点并没有被指定要调用的实际方法。

当Java虚拟机要执行invokedynamic指令时,首先需要链接其对应的动态调用点。在链接的时候,Java虚拟机会先调用一个启动方法(bootstrap method)。这个启动方法的返回值是java.lang.invoke.CallSite类的对象。在通过启动方法得到了CallSite之后,通过这个CallSite对象的getTarget方法可以获取到实际要调用的目标方法句柄。有了方法句柄之后,对这个动态调用点的调用,实际上是代理给方法句柄来完成的。也就是说,对invokedynamic指令的调用实际上就等价于对方法句柄的调用,具体来说是被转换成对方法句柄的invoke方法的调用。Java 7中提供了三种类型的动态调用点CallSite的实现,分别是java.lang.invoke.ConstantCallSite、java.lang.invoke.MutableCallSite和java.lang.invoke.VolatileCallSite。这些CallSite实现的不同之处在于所对应的目标方法句柄的特性不同。ConstantCallSite所表示的调用点绑定的是一个固定的方法句柄,一旦链接之后,就无法修改;MutableCallSite所表示的调用点则允许在运行时动态修改其目标方法句柄,即可以重新链接到新的方法句柄上;而VolatileCallSite的作用与MutableCallSite类似,不同的是它适用于多线程情况,用来保证对于目标方法句柄所做的修改能够被其他线程看到。这也是名称中volatile的含义所在,类似于Java中的volatile关键词的作用。

虽然CallSite一般同invokedynamic指令结合起来使用,但是在Java代码中也可以通过调用CallSite的dynamicInvoker方法来获取一个方法句柄。调用这个方法句柄就相当于执行invokedynamic指令。通过此方法可以预先对CallSite进行测试,以保证字节代码中的invokedynamic指令的行为是正确的,毕竟在生成的字节代码中进行调试是一件很麻烦的事情。下面介绍CallSite时会先通过dynamicInvoker方法在Java程序中直接试验CallSite的使用。

先介绍ConstantCallSite的使用。ConstantCallSite要求在创建的时候就指定其链接到的目标方法句柄。每次该调用点被调用的时候,总是会执行对应的目标方法句柄。在代码,创建了一个ConstantCallSite并指定目标方法句柄为引用String类中的substring方法。

ConstantCallSite的使用示例

public void useConstantCallSite() throws Throwable {  
    MethodHandles.Lookup lookup = MethodHandles.lookup();  
    MethodType type = MethodType.methodType(String.class, int.class, int.class);  
    MethodHandle mh = lookup.findVirtual(String.class, "substring", type);  
    ConstantCallSite callSite = new ConstantCallSite(mh);  
    MethodHandle invoker = callSite.dynamicInvoker();  
    String result = (String) invoker.invoke("Hello", 2, 3);  
} 

接下来的MutableCallSite则允许对其所关联的目标方法句柄进行修改。修改操作是通过setTarget方法来完成的。在创建MutableCallSite的时候,既可以指定一个方法类型MethodType,又可以指定一个初始的方法句柄。如果像下面代码中那样指定方法类型,则通过setTarget设置的方法句柄都必须有同样的方法类型。如果创建时指定的是初始的方法句柄,则之后设置的其他方法句柄的类型也必须与初始的方法句柄相同。MutableCallSite对象中的目标方法句柄的类型总是固定的。下面的代码通过setTarget方法把目标方法句柄分别设置为Math类中的max和min方法,在调用MutableCallSite时可以得到不同的结果。

MutableCallSite的使用示例

public void useMutableCallSite() throws Throwable {  
    MethodType type = MethodType.methodType(int.class, int.class, int.class);  
    MutableCallSite callSite = new MutableCallSite(type);  
    MethodHandle invoker = callSite.dynamicInvoker();  
    MethodHandles.Lookup lookup = MethodHandles.lookup();  
    MethodHandle mhMax = lookup.findStatic(Math.class, "max", type);  
    MethodHandle mhMin = lookup.findStatic(Math.class, "min", type);  
    callSite.setTarget(mhMax);  
    int result = (int) invoker.invoke(3, 5); //值为5  
    callSite.setTarget(mhMin);  
    result = (int) invoker.invoke(3, 5); //值为3  
} 

需要考虑的是多线程情况下的可见性问题。有可能在一个线程中对MutableCallSite的目标方法句柄做了修改,而在另外一个线程中不能及时看到这个变化。对于这种情况,MutableCallSite提供了一个静态方法syncAll来强制要求各个线程中MutableCallSite的使用者立即获取最新的目标方法句柄。该方法接收一个MutableCallSite类型的数组作为参数。

如果一个目标方法句柄可变的调用点被设计为在多线程的情况下使用,可以直接使用VolatileCallSite,而不使用MutableCallSite。当使用VolatileCallSite的时候,每当目标方法句柄发生变化的时候,其他线程会自动看到这个变化。这与Java中volatile关键词的语义是一样的。这比使用MutableCallSite再加上syncAll方法要简单得多。除了这一点之外,VolatileCallSite的作用与MutableCallSite完全相同。

invokedynamic指令实战

下面将要介绍invokedynamic指令在Java字节代码中的具体使用方式。由于涉及字节代码的生成,这里使用了ASM工具。暂时不会对ASM工具的使用做过多的介绍,在第8章中会进行详细介绍。首先需要提供invokedynamic指令所需的启动方法,如代码清单2-70所示。

invokedynamic指令的启动方法

public class ToUpperCase {  
    public static CallSite bootstrap(Lookup lookup, String name, MethodType type, String value) throws Exception {  
        MethodHandle mh = lookup.findVirtual(String.class, "toUpperCase", MethodType.methodType(String.class)).bindTo(value);  
        return new ConstantCallSite(mh);  
    }  
} 

该启动方法是一个普通的Java类中的方法。该方法的类型声明可以是多种格式。返回值必须是CallSite,而参数则允许多种形式。在典型情况下,前面的3个参数分别是进行方法查找的MethodHandles.Lookup对象、方法的名称和方法的类型MethodType。这3个参数之后的其他参数都会被传递给CallSite对应的方法句柄。在上面的代码中,使用了一个ConstantCallSite,而该调用点所绑定的方法句柄引用的底层方法是String类中的toUpperCase方法。启动方法bootstrap接收一个额外的参数value。这个参数被预先绑定给方法句柄。因此当该方法句柄被调用的时候,不需要额外的参数,而返回结果是对参数value表示的字符串调用toUpperCase方法的结果。

有了启动方法之后,就需要在字节代码中生成invokedynamic指令。代码清单2-71给出的程序会产生一个新的Java类文件ToUpperCaseMain.class。通过java命令可以运行该类文件,输出结果是“HELLO”。

生成使用invokedynamic指令的字节代码

public class ToUpperCaseGenerator {   
    private static final MethodHandle BSM =  
            new MethodHandle(MH_INVOKESTATIC,  
            ToUpperCase.class.getName().replace('.', '/'),  
            "bootstrap",  
            MethodType.methodType(  
            CallSite.class, Lookup.class, String.class, MethodType.class, String.class).toMethodDescriptorString());  

    public static void main(String[] args) throws IOException {  
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);  
        cw.visit(V1_7, ACC_PUBLIC | ACC_SUPER, "ToUpperCaseMain", null, "java/lang/Object", null);  
        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC | ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);  
        mv.visitCode();  
        mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");  
        mv.visitInvokeDynamicInsn("toUpperCase", "()Ljava/lang/String;", BSM, "Hello");  
        mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");  
        mv.visitInsn(RETURN);  
        mv.visitMaxs(0, 0);  
        mv.visitEnd();  
        cw.visitEnd();  

        Files.write(Paths.get("ToUpperCaseMain.class"), cw.toByteArray());  
    }  
} 

上面的代码中包含了大量使用ASM工具的代码,这里只需要关心的是“mv.visitInvokeDynamicInsn(“toUpperCase”, “()Ljava/lang/String;”, BSM, “Hello”);”这行代码。这行代码是用来在字节代码中生成invokedynamic指令的。在调用的时候传入了方法的名称、方法句柄的类型、对应的启动方法和额外的参数“Hello”。在invokedynamic指令被执行的时候,会先调用对应的启动方法,即代码清单中的bootstrap方法。bootstrap方法的返回值是一个ConstantCallSite的对象。接着从该ConstantCallSite对象中通过getTarget方法获取目标方法句柄,最后再调用此方法句柄。在调用visitInvokeDynamicInsn方法时提供了一个额外的参数“Hello”。这个参数会被传递给bootstrap方法的最后一个参数value,用来创建目标方法句柄。当目标方法句柄被调用的时候,返回的结果是把参数“Hello”转换成大写形式之后的值“HELLO”。

从上面这个简单的示例可以看出,invokedynamic指令是如何与方法句柄结合起来使用的。上面的示例只使用了最简单的ConstantCallSite。复杂的示例包括根据参数的值确定需要返回的CallSite对象,或是对已有的MutableCallSite对象的目标方法句柄进行修改等。

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值