Java 反射为什么慢?

什么是反射?  

 Java反射就是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;并且能改变它的属性。而这也是Java被视为动态(或准动态,为啥要说是准动态,因为一般而言的动态语言定义是程序运行时,允许改变程序结构或变量类型,这种语言称为动态语言。从这个观点看,Perl,Python,Ruby是动态语言,C++,Java,C#不是动态语言。)语言的一个关键性质。

640?wx_fmt=png

反射能做什么?   

  我们知道反射机制允许程序在运行时取得任何一个已知名称的class的内部信息,包括包括其modifiers(修饰符),fields(属性),methods(方法)等,并可于运行时改变fields内容或调用methods。那么我们便可以更灵活的编写代码,代码可以在运行时装配,无需在组件之间进行源代码链接,降低代码的耦合度;还有动态代理的实现等等;但是需要注意的是反射使用不当会造成很高的资源消耗! 


为什么会慢?  

Method.invoke()源码

public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
      InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
           checkAccess(caller, clazz, obj, modifiers);
       }
}
MethodAccessor ma = methodAccessor;            
   // read volatile
   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("com.tian.Test");
   Method method = kclass.getMethod("target", int.class);
   method.invoke(null, 0);
 }
}

运行结果(注意上面Class.forName("Test")中的Test就是当前类编译后的.class文件,记得带上目录)

640?wx_fmt=png

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

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

现在对上面的代码进行改动一下

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("com.tian.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, i);

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

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

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

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

总结  

在默认情况下,方法的反射调用为委派实现,委派给本地实现来进行方法调用。在调用超过 15次之后,委派实现便会将委派对象切换至动态实现。这个动态实现的字节码是自动生成的,它将直接使用 invoke 指令来调用目标方法。

方法的反射调用会带来不少性能开销,原因主要有三个:变长参数方法导致的 Object 数组,基本类型的自动装箱、拆箱,还有最重要的方法内联。

参考:

https://www.cnblogs.com/ysocean/p/6516248.html

https://blog.csdn.net/xqlovetyj/article/details/82798864

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

田哥coder

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值