前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >ReflectionUtils提高反射性能!

ReflectionUtils提高反射性能!

原创
作者头像
菜菜的后端私房菜
发布2024-08-21 08:49:25
420
发布2024-08-21 08:49:25
举报
文章被收录于专栏:深入浅出Java

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-CaiCaiJavaGithub-CaiCaiJava,除此之外还有更多Java进阶相关知识,感兴趣的同学可以starred持续关注喔~

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

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

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • ReflectionUtils提高反射性能!
    • 测试性能
      • 分析源码
        • getConstructor
        • ReflectionUtils.accessibleConstructor
        • getDeclaredMethod
        • ReflectionUtils.findMethod
      • 总结
        • 最后(不要白嫖,一键三连求求拉~)
    相关产品与服务
    弹性伸缩
    弹性伸缩(Auto Scaling,AS)为您提供高效管理计算资源的策略。您可设定时间周期性地执行管理策略或创建实时监控策略,来管理 CVM 实例数量,并完成对实例的环境部署,保证业务平稳顺利运行。在需求高峰时,弹性伸缩自动增加 CVM 实例数量,以保证性能不受影响;当需求较低时,则会减少 CVM 实例数量以降低成本。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档