Java MethodHandle 反射性能优化

原创
04/18 13:48
阅读数 1.7K

一、关于指令调用

1、关于JVM指令调用

无论是基于栈的JVM还是还是基于寄存器的DVM,他们除了操作数栈中变量的移动和空间分配、以及程序计数器的不同,基本方法调用的基本指令都是遵守JSR规范的。

在讨论MethodHandle之前,说一下Java动态性缺陷。Java语言是一门偏向静态的语言,他的动态性一致存在很多局限:

  • 泛型缺陷: 前端编译(javac)时泛型被擦除、泛型无法传递、泛型不支持值类型、不支持动态泛型
  • 不支持动态方法替换
  • 不支持普通类的动态代理
  • 生成字节码的工具API相对繁琐等
  • 反射调用校验太多需要生成大量字节码,此外需要验证方法区

2、MethodHandle主要作用

MethodHandle主要解决的是问题:

  1. 反射优化,MethodHandle属于轻量级直接调用字节码,而反射属于重量级,并且完全无法代替反射
  2. 动态方法分派,能做到子类调用父类的方法,即便这个方法被子类复写了。
  3. 调用运行在JVM上的其他语言

 

3、JVM指令调用

我们知道,在JSR标准中,提供了四种调用方法的指令

  • invokestatic : 调用静态方法  (JIT 会对这种指令进行内联优化,当然也要看检查静态引用问题,不需要解释器守护内联)
  • invokevirtual :调用虚方法,一般指非私有方法 和final方法(这种优化难度较高,JIT会通过类层次结构分析 (CHA),尝试将方法去虚化,一旦去虚化意味着和invokespacial指令一样调用,最终通过这种方式实现内联、锁优化,这种内联一般需要解释器守护,防止陷阱出现无法逃逸
  • invokespecial: 在android中,该指令为invokedirect ,调用构造方法和非虚方法 (JIT 会对这种指令进行内联优化,而且不需要解释器守护内联)
  • invokeinterface: 调用接口方法(JIT是否优化不取决于这种调用,而是他的实现invokevirtual)

4、MethodHandle的初衷

MethodHandle本质上是为了调用JVM上其他语言,此外也强调方法分派的能力(注意:只能分派直接父类,JSR版本修改之前可以任意分派到父类的父类)

public class MethodHandleDynamicInvoke {

    public static void main(String[] args) {
        Son son = new Son();
        son.love(Father.class);
        son.love(Son.class);
    }
}

class Father   {
    String thinking() {
        return "Father";
    }
}

class Son extends Father {
    @Override
    String thinking() {
        return "Son";
    }

    public void love(Class target) {
        MethodType methodType = MethodType.methodType(String.class);
        try {
            MethodHandle thinkingMethodHanle = MethodHandles.lookup().findSpecial(target, "thinking", methodType, Son.class).bindTo(Son.this);
            System.out.println("I love " + ((String) thinkingMethodHanle.invokeExact()));
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }
}

输出结果如下

I love Father
I love Son

二、MethodHandle如何优化反射?

1、MethodHandle相关方法

前一节我们我们加单了解了JVM相关指令和MethodHandle解决的问题,接下来我们介绍以下几个重要的API

  • MethodHandles.lookup() ; 方法查询器
  • MethodType.methodType(....);  用于返回值,参数匹配,动态参数,第一个参数是返回值类型,从第二个参数起是方法参数
  • MethodHandle.bindTo(Object obj) ;绑定执行对象,绑定之后,invoke活着invokeExact第一个参数就不能传当前对象了
  • MethodHanle.invoke(...) ; 方法参数类型和返回值必须和MethodType一致,但不同于invokeExact,属于父类的子类不需要转为父类,当然成本是该方法执行效率相当低
  • MethodHandle.invokeExact(...) ;方法参数类型和返回值必须和MethodType完全一致,如果是String必须转为String,不能用Object 代替,如果是Object类型,必须严格使用Object类型(如 invokeExact((Object)("Hello"))   ),必要时进行类型转换,带来的收益是该方法执行效率高

下面给出个例子,展示一下invoke和invokeExact方法的区别

public class Output {

    public static MethodHandle getMethodHandle(Object receiver) throws Throwable {
        //如果Lookup对象
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        //MethodType代表方法的类型(不包含方法名称),其实MethodType是为了确定方法的描述符,例如此方法描述符为:(Ljava/lang/Object;)V
        MethodType methodType = MethodType.methodType(void.class, Object.class);
        //在接收者类中查找一个名为println,指定方法类型的虚方法
        return lookup.findVirtual(receiver.getClass(), "println", methodType).bindTo(receiver);
    }

    public static void main(String[] args) throws Throwable {
        Object sysout = System.out;
        MethodHandle sysOut = getMethodHandle(sysout);
        sysOut.invokeExact((Object) ("Hello Dynamic Invoke : System.out" )); //注意类型转换,否则调用失败
        sysOut.invoke("Hello Dynamic Invoke : System.out" );//无需类型转换

        Object myOut = new MyOuput();
        MethodHandle myOutMethodHandle = getMethodHandle(myOut);
        myOutMethodHandle.invokeExact((Object) ("Hello Dynamic Invoke : MyOuput"));  //注意类型转换,否则调用失败
        myOutMethodHandle.invoke("Hello Dynamic Invoke : MyOuput"); //无需类型转换
    }

}

class MyOuput {
    public void println(Object value) {
        System.out.println("" + value);
    }
}

// invoke 动态调用的方法:这个和反射调用基本一致
// invokeExact 调用的方法:类型、返回值类型严格一直,如果参数或者返回值为Object子类,需要强转为Object

输出结果如下
/*
Hello Dynamic Invoke : System.out
Hello Dynamic Invoke : System.out
Hello Dynamic Invoke : MyOuput
Hello Dynamic Invoke : MyOuput
**/

注意:以上我们调用的是没有返回值的println方法,实际上,如果是调用有返回值的的方法,返回值也需要类型转换

MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodHandle method = lookup.findVirtual(IName.class,"getName",MethodType.methodType(String.class));
System.out.println("lookup = " +(String)method.invokeExact((IName)(new MyOuput())));
  • Lookup findConstructor 、findSpecial 、findVirtual 、findStatic 、findXXXGetter、findXXXSetter ...用于方法查询,其中findSpecial查询非虚方法、findStatic查询静态方法
  • Lookup unreflectXXX 配合反射使用,因为MethodHandle并不能完全代替反射,比如调用不同包和class中的私有方法,需要通过反射,然后转为MethodHandle

2、关于性能优化

MethodHandle 如何做到性能优化的?

  • MethodHandle静态化
  • 提前bindTo性能会更高
  • 调用invokeExact

下面我们给出一个例子,定义一个私有方法,提升反射性能

public class ClassMethodHandle {
    private   int getDoubleVal(int val) {
        return val * 2;
    }
}

测试代码如下


public class MethodHandleTest   {
    static  Method method_getDoubleVal;
    static MethodHandle methodHandle_getDoubleVal;
    static {
        try {
            MethodHandles.Lookup lookup = MethodHandles.lookup();
            method_getDoubleVal =  ClassMethodHandle.class.getDeclaredMethod("getDoubleVal",int.class);
            method_getDoubleVal.setAccessible(true);
            methodHandle_getDoubleVal = lookup.unreflect(method_getDoubleVal); 
        //私有方法不能通过findSpecial访问,需要借助Method Reflection
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

    }
    public static void main(String[] args) throws Throwable {

        long startTime  = System.nanoTime();
        testMethodReflection();
        long dx1 = System.nanoTime() - startTime;
        System.out.println("[1] " + (dx1));

        startTime  = System.nanoTime();
        testMethodHandle();
        long dx2 = System.nanoTime() - startTime;
        System.out.println("[2] " + (dx2));

        System.out.println(">>"+(dx1*1f)/dx2+"<<");

    }

    private static void testMethodReflection() {
        try {

             List<Integer> dataList = Arrays.asList(1, 2, 3, 4, 5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20);
            for (int i=0;i<5;i++) {
                MethodHandleTest.transformRelection(dataList,method_getDoubleVal,new ClassMethodHandle());
            }
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }

    private static void testMethodHandle() throws Throwable {

        List<Integer> dataList = Arrays.asList(1, 2, 3, 4, 5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20);
        for (int i=0;i<5;i++) {
            MethodHandleTest.transform(dataList, methodHandle_getDoubleVal, new ClassMethodHandle());// 方法做为参数
        }

    }

    public static List<Integer> transformRelection(List<Integer> dataList, Method method,Object obj) throws Throwable {
        for (int i = 0; i < dataList.size(); i++) {
            dataList.set(i, (int) method.invoke(obj,dataList.get(i)));//relect invoke
        }
        return dataList;
    }

    public static List<Integer> transform(List<Integer> dataList, MethodHandle handle,Object obj) throws Throwable {
        for (int i = 0; i < dataList.size(); i++) {
            dataList.set(i, (int) handle.invokeExact((ClassMethodHandle)obj,(int)dataList.get(i)));//注意参数类型转换
        }
        return dataList;  
    }  
}

性能测试结果如下,取5次中的最大值

[1] 2952626
[2] 1031642
 >>2.8620646<<

可见,使用invokeExact的性能是反射的2倍多。

进一步使用bindTo优化

如果提前绑定呢?


public class MethodHandleTest {
    static Method method_getDoubleVal;
    static MethodHandle methodHandle_getDoubleVal;

    static {
        try {
            MethodHandles.Lookup lookup = MethodHandles.lookup();
            method_getDoubleVal = ClassMethodHandle.class.getDeclaredMethod("getDoubleVal", int.class);
            method_getDoubleVal.setAccessible(true);
            methodHandle_getDoubleVal = lookup.unreflect(method_getDoubleVal).bindTo(new ClassMethodHandle());
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

    }

    public static void main(String[] args) throws Throwable {

        long startTime = System.nanoTime();
        testMethodReflection();
        long dx1 = System.nanoTime() - startTime;
        System.out.println("[1] " + (dx1));

        startTime = System.nanoTime();
        testMethodHandle();
        long dx2 = System.nanoTime() - startTime;
        System.out.println("[2] " + (dx2));

        System.out.println(" "+((dx1*1f)/dx2));

    }

    private static void testMethodReflection() {
        try {
            ClassMethodHandle classMethodHandle = new ClassMethodHandle();
            List<Integer> dataList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20);
            for (int i = 0; i < 5; i++) {
                MethodHandleTest.transformRelection(dataList, method_getDoubleVal, classMethodHandle);
            }
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
    }

    private static void testMethodHandle() throws Throwable {
        List<Integer> dataList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20);
        for (int i = 0; i < 5; i++) {
            MethodHandleTest.transform(dataList, methodHandle_getDoubleVal);// 方法做为参数
        }

    }

    public static List<Integer> transformRelection(List<Integer> dataList, Method method, Object obj) throws Throwable {
        for (int i = 0; i < dataList.size(); i++) {
            dataList.set(i, (int) method.invoke(obj, dataList.get(i)));//relect invoke
        }
        return dataList;
    }

    public static List<Integer> transform(List<Integer> dataList, MethodHandle handle) throws Throwable {
        for (int i = 0; i < dataList.size(); i++) {
            dataList.set(i, (int) handle.invokeExact((int) dataList.get(i)));//注意参数类型转换
        }
        return dataList;
    }
}

性能测试,取5次中的最大值

[1] 5893674
[2] 1279878
 4.6048717

利用静态化的MethodHandle 和bindTo,最终通过invokeExact可以达到4倍左右的性能提升。

 

三、MethodHandle是否可以用于Android呢?

Android虚拟机和JVM在反射调用的时候有很多区别,DVM移除了很多安全检查和校验,速度本身就很快。我们在Android系统中测试之后,发现使用MethodHandle之后,发现远不如原生反射,因此不推荐在Android系统中使用。

 

 

展开阅读全文
打赏
0
1 收藏
分享
加载中
更多评论
打赏
0 评论
1 收藏
0
分享
返回顶部
顶部