反射对于开发人员来说可以说是打开潘多拉魔盒的钥匙,因为反射可以在运行时获得私有方法/私有变量,能够动态修改程序,因此我们都说反射是不安全的。
反射的定义
通常的使用方法:
- Method method = XXX.class.getDeclaredMethod(xx,xx);
- method.invoke(target,params);
反射的获取
须知道,想使用反射必须获取 Class 对象,而对于每个 Class 对象而言,无论创建多少个实例对象,在Java虚拟机中都只对应一个 Class 对象。
通常我们调用反射的时候,会先创建 class 对象,然后再获取它 的method 对象,接着调用 invoke 方法。
如果查阅 Method.invoke 方法,则会发现实际上委派给了 MethodAccessor 处理。MethodAccessor是一个接口,有两个具体实现:一个通过本地方法实现反射调用,一个通过委派模式。为了方便,后面都称为:本地实现/委派实现。
在第一次调用反射的时候,会调用委派实现,然后再将请求传到本地方法实现,最后再传给目标方法使用。这里有一个值得关注的点,为什么不直接调用本地实现,而是需要委派实现作为中间层呢?
原因其实也不难,主要是 Java 的反射调用机制还有一种动态生成字节码的实现(下称动态实现),也就是直接使用 invoke 指令来调用目标方法。之所以采用委派实现,便是为了能够在“本地实现”和动态实现之间来回切换。
动态实现与本地实现的区别在于,反射代码段重复运行15次以上就会使用动态实现,15次以下就使用本地实现。考虑到许多反射调用仅会执行一次,Java 虚拟机设置了一个阈值 15(可以通过 -Dsun.reflect.inflationThreshold= 来调整)
反射的缺点
反射的优点有很多,缺点也不少,这使得开发人员在使用反射时需要分外小心。 主要缺点有以下几个方面:
- 安全:反射使得程序运行在一个没有安全限制的环境之下。
- 性能开销:由于反射需要涉及类的动态解析,而且还要检查方法可见性,同时会对参数做封装和解封。
- 可移植性:随着JDK版本升级,涉及到反射的类库可能会影响原本的代码逻辑,即失去原本功能。
反射的优化
(1) 选择合适的api:获取反射元数据的时候避免使用遍历的方法,如:
- 使用
Class.getField()替代Class.getFields()。 - 使用
Class.getMethod()替代Class.getMethods()。 - 使用
Class.getConstructor()替代Class.getConstructors()。 简单来说,就是尽可能避免返回整个集合或者数组,除非想要获取 Class 的所有 Field、Method 或者 Constructor,这样做可以降低由于遍历或者判断带来的性能损耗。
(2) 使用缓存机制缓存反射时候操作相关元数据:
这个其实没什么好说的,因为整个反射过程中最耗时的便是获取元数据这一阶段,如 Class.forName() 。
(3) 使用直接操作替代反射: 这里指的是把反射操作相关元数据直接放置在类的成员变量中,降低从缓存中读取的性能开销,相当于(2)的加强版,比较典型的使用场景有JDK的动态代理。
在应用层面我们能做的优化就只有这些了,剩下的都是 Native 方法的耗时,只能通过升级 JVM(JDK) 、使用JIT编译器等非编码层面的手段提升反射的性能,同时也应该避免在高性能要求的场景下使用反射。