Java反射为什么慢

参考答案

1.  Java反射是什么

  • Reflection(反射) 是 Java 程序开发语言的特征之一,它允许运行中(注意是运行时,而非编译时)的 Java 程序对自身进行检查,或者说“自审”,并能直接操作程序的内部属性。

2.  Java反射的作用

  • 可以通过它访问Class类中的方法,甚至通过setAccessible方法、在该类之外访问该类的私有方法;
  • IDE中,当你在一个类对象之后输入“.”之后,显示的方法、属性,也是通过该功能实现的;
  • 方法调试过程中,它能够枚举某一对象中所有的字段;
  • 很多java开源框架,为了保证可扩展性,都是通过反射和配置文件来加载不同类,比如Spring等。

3.  Java反射为什么慢

  • Java反射在日常开发中是经常用到的技术点,这包括spring的Ioc、一些除cglib之外的bean copy(cglib采用asm动态生成字节码来实现)。
  • 在spring的ioc中,我们或许无法感知到,这是因为大部分类实例都是单例,只在容器启动的时候加载一次,并在容器内缓存它的实例。
  • 但是,在业务code中的beancopy则不然。请求量大的情况下,很多线程栈都会在这个位置慢下来,并且消耗较高的cpu。这也就是反射慢引起的。
  • 那么反射为什么慢呢?我们通过下面这个实例来详解:

Method.invoke的源码:

public final class Method extends Executable {
...
public Object invoke(Object obj, Object... args) throws ... {
    ... // 权限检查
    MethodAccessor ma = methodAccessor;
    if (ma == null) {
        ma = acquireMethodAccessor();
    }
    return ma.invoke(obj, args);
    }
}

其中的MethodAccessor 是一个接口,它有两个已有的具体实现:一个通过本地方法来实现反射调用,另一个则使用了委派模式。

每个Method实例第一次反射调用时,都会生成一个委派实现,通过该实现、传入参数,就能进入目标方法。

import java.lang.reflect.Method;

public class Test {
    public static void target(int i){
        new Exception("#"+i).printStackTrace();
    }
    public static void main(String[] args) throws Exception{
        Class<?> kclass = Class.forName("Test");
        Method method = kclass.getMethod("target",int.class);
        method.invoke(null,0);
    }
}

结果:

java.lang.Exception: #0
    at Test.target(Test.java:5)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at Test.main(Test.java:10)

上述通过异常信息,打印了堆栈信息,能看到其先进入委托(DelegatingMethodAccessorImpl),然后是本地实现(NativeMethodAccessorImpl),最后到达目标方法。中间多了一个委托,就是因为之前提到的反射的另一个实现:动态实现。

动态实现的执行效率比本地实现快很多,因其无需经过Java到C++再到Java的切换(换言之,本地实现快,从本地实现的NativeMethod就可以看出其为C++实现),没有经过这个过程,所以速度快很多。

//vm options:-verbose:class -Dsun.reflect.inflationThreshold=5
 public static void main(String[] args) throws Exception{
        Class<?> kclass = Class.forName("Test");
        Method method = kclass.getMethod("target",int.class);

        for (int i = 0; i < 17; i++) {
            method.invoke(null,i);
        }
    }

那为什么仅调用一次(或者少数几次的时候),没有使用动态实现方式呢?因为这种方式生成的字节码十分耗时,仅一次的话,反而是本地实现要快3到4倍。根据JVM一贯的风格,就会设置一个阈值(15,当然也是可以通过vm参数调整-Dsun.reflect.inflationThreshold=),到达这个设定值后,就会使用动态实现的实现,虚拟机会加载很多类,成为inflation过程(可通过-Dsun.reflect.noInflation=true关闭)。

所以反射的开销如下:

  • Class.forName,调用本地方法,耗时
  • Class.getMethod,遍历该类的共有方法,匹配不到,遍历父类共有方法, 耗时,getMethod会返回得到结果的拷贝,应避免getMethods和getDeclardMethods方法,减少不必要堆空间消耗。
  • Method.invoke
method.invoke(null,128);

 

将invoke的参数改变时,查看其中字节码,发现多了新建Object数据和int类型装箱的指令。

原因为

  • Method.invoke是一个变长参数方法,字节码层面它的最后一个参数是object数组,所以编译器会在方法调用处生成一个数据,传入;
  • Object数组不能存储基本类型,所以会自动装箱

这两者都会带来性能开销,也会占用堆内存,加重gc负担。但是实际上述例子并不会触发gc,因为原本的反射调用被内联,其创建的对象被虚拟机认为“不会逃逸”,此时会将其优化为栈上分配(非堆上分配),不会触发gc。

针对上诉实例中的优化,可以关闭inflation,直接采用动态实现方式进行反射;同时可以关闭方法的权限检查(private等)。针对动态装箱问题,虚拟机会缓存-128-127之间的Integer对象,更改实例中的128,或调整虚拟机环境的数值范围、或者手动缓存128都可以进行相应优化,提高反射性能。

方法的反射调用会带来不少性能开销,总结原因主要有三个:

  • 变长参数方法导致的 Object 数组;
  • 基本类型的自动装箱、拆箱;
  • 还有最重要的方法内联。

以上,是Java面试题【 Java反射为什么慢】的参考答案。

 

输出,是最好的学习方法。

欢迎在评论区留下你的问题、笔记或知识点补充~

—end—

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧