1. 背景
今天刷到一篇文章,标题是反射为什么慢,一下子懵逼了,确实没想过这个问题;盲猜了一下是由于反射实际上是做了一个代理的动作,导致执行的效率是小于直接实体类去调用方法的。
2. 文章给出的解释
文章中给出的理由是因为以下4点:
- 反射涉及动态解析的内容,不能执行某些虚拟机优化,例如JIT优化技术
- 在反射时,参数需要包装成object[]类型,但是方法真正执行的时候,又使用拆包成真正的类型,这些动作不仅消耗时间,而且过程中会产生很多的对象,这就会导致gc,gc也会导致延时
- 反射的方法调用需要从数组中遍历,这个遍历的过程也比较消耗时间
- 不仅需要对方法的可见性进行检查,参数也需要做额外的检查
3. 结合实际理解
3.1 第一点分析
首先我们需要知道,java中的反射是一种机制,它可以在代码运行过程中,获取类的内部信息(变量、构造方法、成员方法);操作对象的属性、方法。 然后关于反射的原理,首先我们需要知道一个java项目在启动之后,会将class文件加载到堆中,生成一个class对象,这个class对象中有一个类的所有信息,通过这个class对象获取类相关信息的操作我们称为反射。
其次是JIT优化技术,首先我们需要知道在java虚拟机中有两个角色,解释器和编译器;这两者各有优劣,首先是解释器可以在项目启动的时候直接直接发挥作用,省去编译的时候,立即执行,但是在执行效率上有所欠缺;在项目启动之后,随着时间推移,编译器逐渐将机器码编译成本地代码执行,减少解释器的中间损耗,增加了执行效率。
我们可以知道JIT优化通常依赖于在编译时能够知道的静态信息,而反射的动态性可能会破坏这些假设,使得JIT编译器难以进行有效的优化。
3.2 第二点
关于第二点,我们直接写一段反射调用对象方法的demo:
java
@Test
public void methodTest() {
Class clazz = MyClass.class;
try {
//获取指定方法
//这个注释的会报错 java.lang.NoSuchMethodException
//Method back = clazz.getMethod("back");
Method back = clazz.getMethod("back", String.class);
Method say = clazz.getDeclaredMethod("say", String.class);
//私有方法需要设置
say.setAccessible(true);
MyClass myClass = new MyClass("abc", 99);
//反射调用方法
System.out.println(back.invoke(myClass, "back"));
say.invoke(myClass, "hello world");
} catch (Exception e) {
e.printStackTrace();
}
}
在上面这段代码中,我们调用了一个invoke 方法,并且传了class对象和参数,进入到invoke方法中,我们可以看到invoke方法的入参都是Object类型的,args更是一个Object 数组,这就第二点,关于反射调用过程中的拆装箱。
java
@CallerSensitive
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);
}
3.3 第三点
关于调用方法需要遍历这点,还是上面那个demo,我们在获取Method 对象的时候是通过调用getMethod、getDeclaredMethod方法,点击进入这个方法的源码,我们可以看到如下代码:
java
private static Method searchMethods(Method[] methods,
String name,
Class<?>[] parameterTypes)
{
Method res = null;
String internedName = name.intern();
for (int i = 0; i < methods.length; i++) {
Method m = methods[i];
if (m.getName() == internedName
&& arrayContentsEq(parameterTypes, m.getParameterTypes())
&& (res == null
|| res.getReturnType().isAssignableFrom(m.getReturnType())))
res = m;
}
return (res == null ? res : getReflectionFactory().copyMethod(res));
}
我们可以看到,底层实际上也是将class对象的所有method遍历了一遍,最终才拿到我们需要的方法的,这也就是第二点,执行具体方法的时候需要遍历class对象的方法。
3.4 第四点
第4点说需要对方法和参数进行检查,也就是我们在执行具体的某一个方法的时候,我们实际上是需要校验这个方法是否可见的,如果不可见,我们还需要将这个方法设置为可见,否则如果我们直接调用这个方法的话,会报错。
同时还有一个点,在我们调用invoke方法的时候,反射类会对方法和参数进行一个校验,让我们来看一下源码:
java
@CallerSensitive
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);
}
我们可以看到还有quickCheckMemberAccess、checkAccess 等逻辑
4. 总结
平时在反射这块用的比较少,也没针对性的去学习一下。在工作之余,还是得保持一个学习的习惯,这样子才不会出现今天这种被一个问题难倒的情况,而且才能产出更多、更优秀的方案。