ReflectionUtils提高反射性能!

2024-08-21 08:49:25 浏览数 (2)

ReflectionUtils提高反射性能!

有一次小菜遇上一个通用的需求,于是决定在项目中使用反射,等到小菜提交代码后,审核代码的技术leader直摇头,又把小菜给叫过去了

技术leader:小菜同学,项目里用反射性能是会变慢的,但有时候为了通用性是可以用反射的,原生的反射API性能没那么好,我们可以使用Spring框架封装的ReflectionUtils工具类

小菜嘀嘀咕咕的走回工位:这个老登儿,上次就让我改成BigDecimal,这次又要我改成ReflectionUtils

算了,工欲善其事,必先利其器,让我先来看看这个ReflectionUtils到底快多少

测试性能

先写下一个实体类(省略方法),通过反射来创建实例,并通过反射修改字段的数据

代码语言:java复制
public class ReflectionObject {
    private String name;
    private int age;
}

先写下原生反射的代码:

  1. 先使用构造器创建实例
  2. 再通过Method调用方法修改字段数据
  3. 直接修改字段数据
代码语言:java复制
private static void jdkReflection() {
    Class<ReflectionObject> objectClass = ReflectionObject.class;
    try {
        //通过构造器创建实例
        Constructor<ReflectionObject> constructor = objectClass.getConstructor();
        ReflectionObject instance = constructor.newInstance();

        //调用方法
        Method setNameMethod = objectClass.getDeclaredMethod("setName", String.class);
        setNameMethod.invoke(instance, "菜菜的后端私房菜");

        //修改字段
        Field field = objectClass.getDeclaredField("age");
        field.setAccessible(true);
        field.set(instance, 18);
    } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException | InstantiationException |
             NoSuchFieldException e) {
        throw new RuntimeException(e);
    }
}

经过测试原生反射的性能如下表:

调用方法次数

1

1_000

10_000

1_000_000

10_000_000

耗时ms

2

4

12

285

3198

通过这个表格使用反射1W次才12ms,100W次285ms,1kw次3.198s

平时通过反射也不会创建这么多对象,这样一看反射似乎性能也不差呀

这次测试相当于是在电脑性能最好的时候测的,而且一般服务器没有电脑硬件这么好,因此大量使用反射时的性能开销还是存在的

ReflectionUtils提供的API非常简单、见名知意,小菜上手了一会就写出与原生反射类似的代码:

代码语言:java复制
private static void springReflection() {
    Constructor<ReflectionObject> constructor = null;
    ReflectionObject instance = null;
    try {
        //使用构造创建实例
        constructor = ReflectionUtils.accessibleConstructor(ReflectionObject.class);
        instance = constructor.newInstance();
    } catch (NoSuchMethodException | InvocationTargetException | InstantiationException |
             IllegalAccessException e) {
        throw new RuntimeException(e);
    }

    //找到方法并调用
    Method setNameMethod = ReflectionUtils.findMethod(ReflectionObject.class, "setName", String.class);
    if (Objects.nonNull(setNameMethod)) {
        ReflectionUtils.invokeMethod(setNameMethod, instance, "菜菜的后端私房菜");
    }

    //找到字段设置值
    Field field = ReflectionUtils.findField(ReflectionObject.class, "age");
    if (Objects.nonNull(field)) {
        ReflectionUtils.makeAccessible(field);
        ReflectionUtils.setField(field, instance, 18);
    }
}

经过测试ReflectionUtils与原生反射的性能对比如下表:

调用方法次数

1

1_000

10_000

1_000_000

10_000_000

原生耗时ms

2

4

12

285

3198

ReflectionUtils耗时ms

49

4

8

74

495

经过对比可以发现:ReflectionUtils首次初始化会慢很多,但是后续反射比原生API快

当调用方法次数达到千万次时,原生反射耗时比ReflectionUtils多6倍多

分析源码

ReflectionUtils究竟是如何封装的,怎么会比原生反射快这么多?

小菜百思不得其解于是决定查看源码进行分析原因

打开 ReflectionUtils ,发现其有非常多的方法和字段,其中重要的两个字段:

代码语言:java复制
private static final Map<Class<?>, Method[]> declaredMethodsCache = new ConcurrentReferenceHashMap<>(256);
private static final Map<Class<?>, Field[]> declaredFieldsCache = new ConcurrentReferenceHashMap<>(256);

这两个字段是缓存,declaredMethodsCache分别存储Class对象以及对应的方法数组,而declaredFieldsCache存储Class对象和对应的字段数组

小菜心想:难道ReflectionUtils是通过缓存来加快速度的?难道反射通过Class获取方法数组和字段数组的用时很长?

剩下的方法看不出个所以然,于是小菜决定从案例中的方法对比进行查看:

getConstructor

小菜先从原生API获取构造器的方法入手

代码语言:java复制
public Constructor<T> getConstructor(Class<?>... parameterTypes)
    throws NoSuchMethodException, SecurityException {
    //安全管理器检查访问权限
    checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), true);
    //获取构造器
    return getConstructor0(parameterTypes, Member.PUBLIC);
}

在checkMemberAccess方法中会获取安全管理器检查是否允许访问,但默认情况下是没有安全管理器的

接着查看getConstructor0方法:

代码语言:java复制
private Constructor<T> getConstructor0(Class<?>[] parameterTypes,
                                    int which) throws NoSuchMethodException
{
    //会先获取构造器数组
    Constructor<T>[] constructors = privateGetDeclaredConstructors((which == Member.PUBLIC));
    for (Constructor<T> constructor : constructors) {
        //遍历找到参数符合条件的构造器
        if (arrayContentsEq(parameterTypes,
                            constructor.getParameterTypes())) {
            //通过工厂copy对象返回
            return getReflectionFactory().copyConstructor(constructor);
        }
    }
    throw new NoSuchMethodException(getName()   ".<init>"   argumentTypesToString(parameterTypes));
}
  1. 会先获取构造器数组 privateGetDeclaredConstructors
  2. 遍历找到参数符合条件的构造器 arrayContentsEq
  3. 通过工厂copy对象返回 copyConstructor

主要查看privateGetDeclaredConstructors 获取构造器数组的流程:

代码语言:java复制
private Constructor<T>[] privateGetDeclaredConstructors(boolean publicOnly) {
    //检查初始化
    checkInitted();
    Constructor<T>[] res;
    //获取反射数据
    ReflectionData<T> rd = reflectionData();
    if (rd != null) {
        res = publicOnly ? rd.publicConstructors : rd.declaredConstructors;
        //存在数据直接返回 没存在后续要查询 相当于ReflectionData是缓存
        if (res != null) return res;
    }
    // No cached value available; request value from VM
    if (isInterface()) {
        @SuppressWarnings("unchecked")
        Constructor<T>[] temporaryRes = (Constructor<T>[]) new Constructor<?>[0];
        res = temporaryRes;
    } else {
        //不是接口 调用本地方法获取构造器数组
        res = getDeclaredConstructors0(publicOnly);
    }
    
    //查到数据 把数据放到缓存 ReflectionData
    if (rd != null) {
        if (publicOnly) {
            rd.publicConstructors = res;
        } else {
            rd.declaredConstructors = res;
        }
    }
    return res;
}

在获取构造器数组的方法中使用ReflectionData作为缓存,如果存在数据就返回,如果不存在则要调用本地方法进行查询

查看ReflectionData字段可以发现,不止构造器使用缓存,不同访问权限的字段和方法也会使用缓存

代码语言:java复制
private static class ReflectionData<T> {
    volatile Field[] declaredFields;
    volatile Field[] publicFields;
    volatile Method[] declaredMethods;
    volatile Method[] publicMethods;
    volatile Constructor<T>[] declaredConstructors;
    volatile Constructor<T>[] publicConstructors;
    // Intermediate results for getFields and getMethods
    volatile Field[] declaredPublicFields;
    volatile Method[] declaredPublicMethods;
    volatile Class<?>[] interfaces;

    // Value of classRedefinedCount when we created this ReflectionData instance
    final int redefinedCount;

    ReflectionData(int redefinedCount) {
        this.redefinedCount = redefinedCount;
    }
}
ReflectionUtils.accessibleConstructor

再来看看ReflectionUtils是如何获取构造器的

代码语言:java复制
public static <T> Constructor<T> accessibleConstructor(Class<T> clazz, Class<?>... parameterTypes)
       throws NoSuchMethodException {
	//调用原生获取构造器
    Constructor<T> ctor = clazz.getDeclaredConstructor(parameterTypes);
    //设置允许访问
    makeAccessible(ctor);
    return ctor;
}
  1. 调用原生API获取构造器,只是访问权限为 DECLARED 而不是 public
  2. 担心访问权限不足,设置允许访问

通过获取构造器的方法进行比较,小菜认为ReflectionUtils的API反而多了一步makeAccessible,会更耗时

于是进行只获取构造器的测试:

调用方法次数

1

1_000

10_000

1_000_000

10_000_000

原生耗时ms

1

2

4

17

76

ReflectionUtils耗时ms

42

1

3

44

251

由此可以看出ReflectionUtils带来的性能提升并不是在获取构造器上,那只能是“问题”出在方法Method和字段Field上了

getDeclaredMethod

继续查看原生API获取方法的源码:

代码语言:java复制
public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
    throws NoSuchMethodException, SecurityException {
    checkMemberAccess(Member.DECLARED, Reflection.getCallerClass(), true);
    Method method = searchMethods(privateGetDeclaredMethods(false), name, parameterTypes);
    if (method == null) {
        throw new NoSuchMethodException(getName()   "."   name   argumentTypesToString(parameterTypes));
    }
    return method;
}
  1. 通过安全管理器检查是否允许访问 checkMemberAccess (前面已经说过)
  2. 通过缓存获取方法数组 privateGetDeclaredMethods(类似构造器,都是使用ReflectionData做缓存)
  3. 查找方法 searchMethods

继续查看searchMethods流程与构造器类似:

代码语言: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));
}
  1. 遍历查找方法
  2. 找到方法后通过工厂创建对象返回

总结一下可能耗时的操作:

  1. ReflectionData缓存中不存在 (第一次获取方法数组会去调用本地方法获取)
  2. 遍历查找方法 (如果方法太多可能开销大)
  3. 通过工厂copy创建对象返回(临时、复杂对象创建的开销)
ReflectionUtils.findMethod

再来查看ReflectionUtils的API查找方法与原生有什么区别

代码语言:java复制
public static Method findMethod(Class<?> clazz, String name, @Nullable Class<?>... paramTypes) {
    Assert.notNull(clazz, "Class must not be null");
    Assert.notNull(name, "Method name must not be null");
    Class<?> searchType = clazz;
    //当前类找不到去找父类
    while (searchType != null) {
       //获取方法数组
       Method[] methods = (searchType.isInterface() ? searchType.getMethods() :
             getDeclaredMethods(searchType, false));
       //循环查找比较 
       for (Method method : methods) {
          if (name.equals(method.getName()) && (paramTypes == null || hasSameParams(method, paramTypes))) {
             return method;
          }
       }
       //当前类找不到去找父类
       searchType = searchType.getSuperclass();
    }
    return null;
}
  1. 获取方法数组,如果是接口调用getMethods(会去调原生API并copy),否则调用getDeclaredMethods
  2. 循环查找比较,找到后返回,找不到找父类

小菜心想:我还以为会在循环上做文章呢,结果也是循环查找,复杂度与方法数量有关

继续查看getDeclaredMethods:

代码语言:java复制
private static Method[] getDeclaredMethods(Class<?> clazz, boolean defensive) {
    Assert.notNull(clazz, "Class must not be null");
    //从缓存中获取
    Method[] result = declaredMethodsCache.get(clazz);
    if (result == null) {
       try {
          //调用原生API查到方法数组后生成新的实例
          Method[] declaredMethods = clazz.getDeclaredMethods();
          //获取接口中的方法
          List<Method> defaultMethods = findConcreteMethodsOnInterfaces(clazz);
        
          //处理结果 
          if (defaultMethods != null) {
             result = new Method[declaredMethods.length   defaultMethods.size()];
             System.arraycopy(declaredMethods, 0, result, 0, declaredMethods.length);
             int index = declaredMethods.length;
             for (Method defaultMethod : defaultMethods) {
                result[index] = defaultMethod;
                index  ;
             }
          }
          else {
             result = declaredMethods;
          }
          //结果放入缓存
          declaredMethodsCache.put(clazz, (result.length == 0 ? EMPTY_METHOD_ARRAY : result));
       }
       catch (Throwable ex) {
          throw new IllegalStateException("Failed to introspect Class ["   clazz.getName()  
                "] from ClassLoader ["   clazz.getClassLoader()   "]", ex);
       }
    }
    //defensive为false 直接返回结果,不会clone
    return (result.length == 0 || !defensive) ? result : result.clone();
}
  1. 查询缓存,有结果直接返回
  2. 没有结果,调用原生API查询并合并接口中的方法,处理结果后放入缓存

经过小菜的细心比较:找到方法后原生API总是用工厂去创建getReflectionFactory().copyMethod(res),而ReflectionUtils会调用原生方法getDeclaredMethods提前把方法数组创建好放到缓存中,后续找到直接返回

小菜继续向下翻看源码,但是发现 ReflectionUtils 调用方法的API也是去调用原生的,没有区别

小菜继续查看获取字段以及设置相关的源码,发现与方法类似

小菜心想:难道每次多创建复杂对象竟然会造成这么大的开销?难道是频繁创建对象导致gc?

突然小菜认为是JVM参数未设置,突然增加这么多对象,肯定是会堆扩容和GC的

小菜后续又试了一下千万次循环的数据有下降,但是差不多只有几十毫秒影响不大

不甘心的小菜又重读了一遍源码,最后发现原生反射的缓存ReflectionData是软引用

这就说明当gc发生,缓存会被清空,导致需要重新加载从而影响性能

代码语言:java复制
private ReflectionData<T> reflectionData() {
    //软引用
    SoftReference<ReflectionData<T>> reflectionData = this.reflectionData;
    int classRedefinedCount = this.classRedefinedCount;
    ReflectionData<T> rd;
    if (useCaches &&
        reflectionData != null &&
        (rd = reflectionData.get()) != null &&
        rd.redefinedCount == classRedefinedCount) {
        return rd;
    }
    // else no SoftReference or cleared SoftReference or stale ReflectionData
    // -> create and replace new instance
    return newReflectionData(reflectionData, classRedefinedCount);
}

除了这些因素,反射动态解析类元数据加载到内存生成Class,也会错过一些诸如JIT编译器的性能优化

至此我们分析完ReflectionUtils提高反射性能的诀窍,以后在项目中遇到需要使用反射时可以使用ReflectionUtils~

总结

反射是需要检查访问权限的,比如说私有字段是否允许访问

使用反射进行方法调用时通常是Object,因此会涉及到需要强制类型转换

JIT即时编译器会将循环次数多的热点代码进行编译成本地码,而后续不再需要解释执行,从而进行优化

反射需要运行时动态解析类的元数据并查找,动态解析导致可能无法使用JIT

为了安全,反射调用本地方法查找方法、字段数组时,通常会将对象进行copy后返回新的实例

原生反射使用软引用作为缓存,虽然适合内存弹性伸缩,但是gc时会导致缓存丢失需要重新加载,而ReflectionUtils的缓存是强引用不会因为gc而丢失

原生反射为了安全性在找到对象时会使用工厂创建新对象返回,而ReflectionUtils缓存数组时提前全部copy创建新对象,在找到对象后是直接返回,避免创建对象,从而减少gc

最后(不要白嫖,一键三连求求拉~)

本篇文章被收入专栏 Java,感兴趣的同学可以持续关注喔

本篇文章笔记以及案例被收入 Gitee-CaiCaiJava、 Github-CaiCaiJava,除此之外还有更多Java进阶相关知识,感兴趣的同学可以starred持续关注喔~

有什么问题可以在评论区交流,如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~

关注菜菜,分享更多技术干货,公众号:菜菜的后端私房菜

0 人点赞