MethodHandle与invokedynamic指令

转载自:https://blog.csdn.net/yushuifirst/article/details/48028859?utm_source=blogxgwz7?utm_medium=distribute.pc_relevant.none-task-blog-baidujs-1

  

MethodHandle

  MethodHandle即方法句柄,使用方法和效果上与Reflection都有众多相似之处。不过,它们也有以下这些区别:

  1. Reflection和MethodHandle机制本质上都是在模拟方法调用,但是Reflection是在模拟Java语言层面的方法调用,而MethodHandle是在模拟Java字节码层面的方法调用,MethodHandle效率更高。
    在MethodHandles.Lookup上的三个方法findStatic()、findVirtual()、findSpecial(),正是为了对应于invokestatic、invokevirtual & invokeinterface和invokespecial这几条字节码指令的执行权限校验行为,而这些底层细节在使用Reflection API时是不需要关心的。

invokespecial:调用一个初始化(构造)方法,私有方法或者父类的方法
invokestatic:调用静态方法
invokevirtual:调用实例方法
invokeinterface:调用接口方法

  1. Reflection中的java.lang.reflect.Method对象远比MethodHandle机制中的java.lang.invoke.MethodHandle对象所包含的信息来得多。前者是方法在Java端的全面映像,包含了方法的签名、描述符以及方法属性表中各种属性的Java端表示方式,还包含有执行权限等的运行期信息。而后者仅仅包含着与执行该方法相关的信息。用开发人员通俗的话来讲,Reflection是重量级,而MethodHandle是轻量级。
  2. 和反射相比好处是:调用 invoke() 已经被JVM优化,类似直接调用一样,性能要好很多。

  
  方法句柄中首先涉及到两个重要的类,MethodHandle和MethodType。
  MethodHandle:它是可对直接执行的方法的类型引用,或者说,它是一个有能力安全调用方法的对象。换个方法来说,通过句柄可以直接调用该句柄所引用的底层方法。从作用上来看,方法句柄类似于反射中的Method类,但是方法句柄的功能更加强大、使用更加灵活、性能也更好。
  MethodType:它是表示方法签名类型的不可变对象。每个方法句柄都有一个MethodType实例,用来指明方法的返回类型和参数类型。它的类型完全由参数类型和方法类型来确定,而与它所引用的底层的方法的名称和所在的类没有关系。举个例子,例如String类的length方法和Integer类的intValue方法的方法句柄的类型就是一样的,因为这两个方法都没有参数,而且返回值类型都是int。
  简单示例:

    public class User {
        public void say(String msg) {
            System.out.println("hello world, " + msg);
        }
    }
    public static void test0() throws Throwable {
        // 通过MethodType的静态工厂方法构造 MethodType
        MethodType methodType = MethodType.methodType(void.class, String.class);

        // 获取方法句柄
        MethodHandle methodHandle = MethodHandles.lookup()
            .findVirtual(User.class, "say", methodType);

        User user = new User();
        methodHandle.invoke(user, "zero");

        // invoke和invokeExact方法, invokeExact方法与直接调用底层方法是一样的
        methodHandle.invokeExact(user, "zero");
    }

  

invokedynamic指令

  invokedynamic指令需要与MethodHandle方法句柄结合起来使用。该指令的灵活性在很大程度上取决于方法句柄的灵活性。
  在字节代码中每个出现的 invokedynamic 指令都成为一个动态调用点(dynamic call site)。每个动态调用点在初始化的时候,都处于未链接的状态。在这个时候,这个动态调用点并没有被指定要调用的实际方法。
  当Java虚拟机要执行 invokedynamic 指令时,首先需要链接其对应的动态调用点。在链接的时候,Java虚拟机会先调用一个启动方法(bootstrap method)。这个启动方法的返回值是 java.lang.invoke.CallSite 类的对象。在通过启动方法得到了CallSite之后,通过这个CallSite对象的 getTarget 方法可以获取到实际要调用的目标方法句柄。有了方法句柄之后,对这个动态调用点的调用,实际上是代理给方法句柄来完成的。也就是说,对invokedynamic指令的调用实际上就等价于对 方法句柄 的调用,具体来说是被转换成对方法句柄的invoke方法的调用。
  Java 7中提供了三种类型的动态调用点CallSite的实现,分别是java.lang.invoke.ConstantCallSitejava.lang.invoke.MutableCallSitejava.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方法。

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时可以得到不同的结果。

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所示。

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对象的目标方法句柄进行修改等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值