java method handle

JDK 7以前字节码指令集中,四条方法调用指令invokevirtualinvokespecialinvokestaticinvokeinterface的第一个参数都是被调用的方法的符号引用CONSTANT_Methodref_info或者CONSTANT_InterfaceMethodref_info常量,方法的符号引用在编译时产生,而动态类型语言只有在运行期才能确定接收者类型,为了在Java虚拟机层面上提供动态类型的直接支持,在JDK 7中新增了invokedynamic指令。

JDK 7实现了JSR 292《Supporting Dynamically Typed Languages on the Java Platform》,JSR 292主要服务的对象是JVM上动态语言的实现。

新加入的java.lang.invoke包是就是JSR 292的一个重要组成部分,这个包的主要目的是在之前单纯依靠符号引用来确定调用的目标方法这条路之外,提供一种新的动态确定目标方法的机制,称为MethodHandle

MethodHandle可以指向任意方法,提供为方法创建“别名”的能力,可以用统一的方式去调用MethodHandle。此外,MethodHandle还支持组合,可以以适配器的方式将多个MethodHandle串在一起,实现参数过滤、参数转换、返回值转换等许多功能。

目前通过benchmark测试的结果来看,调用MethodHandle的速度是最慢的,距调用接口方法的速度还是有较明显的差距,下面会测试代码详细说明。

MethodHandle对许多JVM的内部实现来说并不是一个全新的概念。要实现JVM,在内部总会保留一些指向方法的指针,JDK 7只是把它(和其它许多JVM里原本就支持的概念)具体化为Java类型暴露给Java代码用而已,这就是所谓的reification

MethodHandle的主要要解决的问题就是要高效的调用编译时已知签名但具体目标未知的方法,其主要还是为JVM上的动态语言的实现服务,并不是面向普通的Java程序员的,换个说法就是对普通Java程序员来说并不是很常用的。

MethodHandle示例

public class SimpleDynamicInvoker {    public static class InvokeDynamicA {        public void array(String[] args) {            int length = args.length;            System.out.println(this.getClass().getSimpleName() + " : arguments length is : " + length);        }    }    public static class InvokeDynamicB {        public void array(String[] args) {            int length = args.length;            System.out.println(this.getClass().getSimpleName() + " : arguments length is : " + length);        }        public static int list(List<String> args) {            int length = args.size();            System.out.println("list size is : " + length);            return length;        }    }    public static void main(String[] args) throws Throwable {        InvokeDynamicB invokeDynamicB = new InvokeDynamicB();        MethodHandles.Lookup lookup = MethodHandles.lookup(); // 1        // instance method invoke        MethodType methodType = MethodType.methodType(void.class, String[].class); // 2        MethodHandle methodHandle = lookup.findVirtual(InvokeDynamicB.class, "array", methodType).bindTo(invokeDynamicB); // 3        methodHandle.invoke(new String[]{"abc", "def", "ghi"}); // 4        // bind the same methodType and methodName to another instance        methodHandle = lookup.findVirtual(InvokeDynamicA.class, "array", methodType).bindTo(new InvokeDynamicA());        methodHandle.invokeExact(new String[]{"abc", "def", "ghi"});        // static method invoke        methodType = MethodType.methodType(int.class, java.util.List.class);        methodHandle = lookup.findStatic(InvokeDynamicB.class, "list", methodType);        List<String> list = Lists.newArrayList("abc", "def", "ghi");        // 必须指明返回类型为int,满足methodType的要求,否则抛异常:expected (List)int but found (List)Object        // Object length = methodHandle.invokeExact(list);        int length = (int) methodHandle.invokeExact(list);        System.out.println(length);    }}

这个例子用到的是MethodHandlesMethodHandles.LookupMethodTypeMethodHandle几个类。代码执行流程:

  1. 调用MethodHandles.lookup()方法,遍历调用栈检查访问权限,然后得到一个MethodHandles.Lookup实例;该对象用于确认创建MethodHandle的实例的类对目标方法的访问权限是否满足要求,并提供搜索目标方法的逻辑。
  2. 指定目标方法的方法类型,得到一个MethodType实例。
  3. 通过MethodHandles.lookup()静态方法得到一个类型为MethodHandles.Lookup的工厂,然后靠它搜索指定的类型、指定的名字、指定的方法类型的方法,得到一个MethodHandle实例。
  4. 调用MethodHandle上的invoke()方法,或者是invokeExact()这样与虚拟机指令直接关联的相关方法。

其中,第1步中调用的MethodType.methodType()方法接收的参数是一组类型,第一个参数是返回类型,后面依次是各个参数类型

第2步调用中MethodType.methodType(void.class, String[].class)得到的就是一个返回类型为void,参数列表为String[]类型的方法类型。而这个方法类型在class字节码文件中的描述符就是:

descriptor: ([Ljava/lang/String;)V

MethodType的实例只代表所有返回值类型参数类型匹配的一类方法的方法类型,自身没有名字;在检查某个方法是否与某个MethodType匹配时只考虑结构,可以算是一种特殊的structural-typing

第2步看起来跟普通的反射很像,但通过反射得到的代表方法的对象是java.lang.reflect.Method的实例,它含有许多跟“执行”没有直接关系的信息,比较笨重;通过Method对象调用方法只是正常方法调用的模拟,所有参数会被包装为一个数组,开销较大。而MethodHandle则是个非常轻量的对象,主要目的就是用来引用方法并调用;通过它去调用方法不会导致参数被包装,原始类型的参数也不会被自动装箱。

MethodHandles.Lookup对象上有三个find方法,包括findStatic()findVirtual()findSpecial(),分别对应invokestaticinvokevirtual & invokeinterfaceinvokespecial这4个虚拟机指令。

注意第3步中findVirtual()方法所返回的MethodHandle的方法类型会通过bindTo(Object)显式绑定到一个具体的实例对象上;调用虚方法的MethodHandle要显式传入方法接收者receiver

而下面的findStatic()方法则不需要再调用bindTo(Object)方法进行对象绑定。

MethodHandle的方法类型不是Java语言的静态类型系统的一部分。虽然它的实例在运行时带有方法类型信息(MethodType),但在编译时Java编译器却不知道这一点。所以在编译时,调用invoke时传入任意个数、任意类型的参数都可以通过编译;但在运行时要成功调用,由Java编译器推断出来的返回值类型参数列表必须与运行时MethodHandle实际的方法类型一致,否则会抛出WrongMethodTypeException,可以参考上述例子最后的注释说明。

从上面的例子看来,使用MethodHandle并没有多少困难,不过看完它的用法之后,自然会与java的反射Reflection产生对比。

仅站在Java语言的角度看,MethodHandle的使用方法和效果上与Reflection都有众多相似之处。不过,它们也有以下这些区别:

  1. ReflectionMethodHandle机制本质上都是在模拟方法调用,但是Reflection是在模拟Java代码层次的方法调用,而MethodHandle是在模拟字节码层次的方法调用。在MethodHandles.Lookup上的三个方法findStatic()findVirtual()findSpecial()正是为了对应于invokestaticinvokevirtual & invokeinterfaceinvokespecial这几条字节码指令的执行权限校验行为,而这些底层细节在使用Reflection API时是不需要关心的。
  2. Reflection中的java.lang.reflect.Method对象远比MethodHandle机制中的java.lang.invoke.MethodHandle对象所包含的信息来得多。前者是方法在Java一端的全面映像,包含了方法的签名、描述符以及方法属性表中各种属性的Java端表示方式,还包含有执行权限等的运行期信息。而后者仅仅包含着与执行该方法相关的信息。用开发人员通俗的话来讲,Reflection是重量级,而MethodHandle是轻量级。
  3. MethodHandleReflection除了上面列举的区别外,最关键的一点在于:Reflection API的设计目标是只为Java语言服务的,而MethodHandle则设计为可服务于所有Java虚拟机之上的语言,其中也包括了Java语言而已。

某种程度上可以说invokedynamic指令与MethodHandle机制的作用是一样的,都是为了解决原有四条invoke*指令方法分派规则固化在虚拟机之中的问题,把如何查找目标方法的决定权从虚拟机转嫁到具体用户代码之中,让用户(包含其他语言的设计者)有更高的自由度。

MethodHandle performance

在下面的测试代码中比较了一下不同方式进行方法调用的效率情况,测试环境说明如下:

操作系统环境

iMac (Retina 5K, 27-inch, Late 2014)Processor: 3.5 GHz Intel Core i5Memory: 8 GB 1600 MHz DDR3Graphics: AMD Radeon R9 M290X 2048 MB

JVM运行参数

-Xms1000M -Xmx1000M -Xmn400M -XX:SurvivorRatio=8

Java版本

 java -version

java version "1.8.0_77"

Java(TM) SE Runtime Environment (build 1.8.0_77-b03)

Java HotSpot(TM) 64-Bit Server VM (build 25.77-b03, mixed mode)

测试代码

/** * -Xms1000M -Xmx1000M -Xmn400M -XX:SurvivorRatio=8 * -Xms1000M -Xmx1000M -Xmn400M -XX:SurvivorRatio=8 -Xint * * @author yuweijun 2016-11-20. */public class BenchmarkProfiler {    private long startTime = 0l;    private long endTime = 0l;    public static void main(String[] args) throws Throwable {        BenchmarkProfiler benchmarkProfiler = new BenchmarkProfiler();        benchmarkProfiler.run(10_000_000);        benchmarkProfiler.run(100_000_000);        benchmarkProfiler.run(1000_000_000);    }    public void timerStart() {        startTime = System.currentTimeMillis();    }    public void timerEnd(String profile) {        endTime = System.currentTimeMillis();        System.out.println(profile + " : " + (endTime - startTime));    }    public void run(int reps) throws Throwable {        System.out.println("+++++++++++++++++++++++" + reps);        timerStart();        reflect(reps);        timerEnd("Reflect");        timerStart();        methodHandle(reps);        timerEnd("MethodHandle");        timerStart();        methodHandleBind(reps);        timerEnd("MethodHandleBind");        timerStart();        methodHandleInt(reps);        timerEnd("MethodHandleInt");        timerStart();        implementDirect(reps);        timerEnd("Direct");        timerStart();        iface(reps);        timerEnd("Interface");        timerStart();        staticMethod(reps);        timerEnd("Static");    }    public void reflect(int reps) throws Exception {        Method m = BenchmarkImpl.class.getMethod("exec", Object.class);        BenchmarkImpl benchmark = new BenchmarkImpl();        for (int i = 0; i < reps; i++) {            // auto-boxing            m.invoke(benchmark, i);        }    }    public void methodHandle(int reps) throws Throwable {        BenchmarkImpl benchmark = new BenchmarkImpl();        MethodType methodType = MethodType.methodType(void.class, Object.class);        MethodHandle mh = MethodHandles.lookup().findVirtual(BenchmarkImpl.class, "exec", methodType);        for (int i = 0; i < reps; i++) {            mh.invokeExact(benchmark, (Object) i);        }    }    public void methodHandleInt(int reps) throws Throwable {        BenchmarkImpl benchmark = new BenchmarkImpl();        MethodType methodType = MethodType.methodType(void.class, int.class);        MethodHandle mh = MethodHandles.lookup().findVirtual(BenchmarkImpl.class, "execInt", methodType).bindTo(benchmark);        for (int i = 0; i < reps; i++) {            // 直接调用 int 则快了一倍,封箱操作花费较多时间            mh.invoke(i);        }    }    public void methodHandleBind(int reps) throws Throwable {        BenchmarkImpl benchmark = new BenchmarkImpl();        MethodType methodType = MethodType.methodType(void.class, Object.class);        MethodHandle mh = MethodHandles.lookup().findVirtual(BenchmarkImpl.class, "exec", methodType).bindTo(benchmark);        for (int i = 0; i < reps; i++) {            mh.invoke(i);        }    }    public void implementDirect(int reps) throws Exception {        Method m = BenchmarkImpl.class.getMethod("exec", Object.class);        BenchmarkImpl benchmark = new BenchmarkImpl();        for (int i = 0; i < reps; i++) {            benchmark.exec(i);        }    }    public void iface(int reps) throws Exception {        IBenchmark m = new BenchmarkImpl();        for (int i = 0; i < reps; i++) {            m.exec(i);        }    }    public void staticMethod(int reps) throws Exception {        for (int i = 0; i < reps; i++) {            BenchmarkImpl.execStatic(i);        }    }}

运行结果

+++++++++++++++++++++++10000000
Reflect : 114
MethodHandle : 132
MethodHandleBind : 68
MethodHandleInt : 37
Direct : 29
Interface : 29
Static : 30

+++++++++++++++++++++++100000000
Reflect : 866
MethodHandle : 542
MethodHandleBind : 541
MethodHandleInt : 348
Direct : 229
Interface : 222
Static : 223

+++++++++++++++++++++++1000000000
Reflect : 5538
MethodHandle : 4438
MethodHandleBind : 5197
MethodHandleInt : 2933
Direct : 2069
Interface : 2043
Static : 2211

从上述Mac电脑JDK 8上的运行结果来看,基于接口方法和实现类方法直接调用的性能基本是一样的,与静态方法的调用效率也很接近,ReflectionMethodHandle执行就稍慢一点,MethodHandle方式进行方法调用的效率目前看上去还是很差,甚至会慢于反射。

如果添加-Xint参数,以解释字节码方式运行 JVM,则效率极低,但是略快于反射。

MethodTypeMethodHandle主要是为了配合invokedynamic使用,以及在 Java8 中实现lambda表达式时使用。

References

  1. MethodHandle - What is it all about
  2. JDK 7的MethodHandle
  3. 解析JDK 7的动态类型语言支持
  4. A First Taste of InvokeDynamic
  5. MethodHandle performance