反射性能变慢了?那是你不会用ReflectionUtils
点击关注公众号,“技术干货”及时达!
前言有一次小菜遇上一个通用的需求,于是决定在项目中使用反射,等到小菜提交代码后,审核代码的技术leader直摇头,又把小菜给叫过去了
技术leader:小菜同学,项目里用反射性能是会变慢的,但有时候为了通用性是可以用反射的,原生的反射API性能没那么好,我们可以使用Spring框架封装的ReflectionUtils工具类
小菜嘀嘀咕咕的走回工位:这个老登儿,上次就让我改成BigDecimal,这次又要我改成ReflectionUtils
算了,工欲善其事,必先利其器,让我先来看看这个ReflectionUtils到底快多少
测试性能先写下一个实体类(省略方法),通过反射来创建实例,并通过反射修改字段的数据
public class ReflectionObject { private String name; private int age;}先写下原生反射的代码:
先使用构造器创建实例再通过Method调用方法修改字段数据直接修改字段数据private static void jdkReflection() { ClassReflectionObject objectClass = ReflectionObject.class; try { //通过构造器创建实例 ConstructorReflectionObject 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); }}经过测试原生反射的性能如下表:
调用方法次数11_00010_0001_000_00010_000_000耗时ms24122853198通过这个表格使用反射1W次才12ms,100W次285ms,1kw次3.198s
平时通过反射也不会创建这么多对象,这样一看反射似乎性能也不差呀
这次测试相当于是在电脑性能最好的时候测的,而且一般服务器没有电脑硬件这么好,因此大量使用反射时的性能开销还是存在的
ReflectionUtils提供的API非常简单、见名知意,小菜上手了一会就写出与原生反射类似的代码:
private static void springReflection() { ConstructorReflectionObject 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与原生反射的性能对比如下表:
调用方法次数11_00010_0001_000_00010_000_000原生耗时ms24122853198ReflectionUtils耗时ms494874495经过对比可以发现:ReflectionUtils首次初始化会慢很多,但是后续反射比原生API快
当调用方法次数达到千万次时,原生反射耗时比ReflectionUtils多6倍多
分析源码ReflectionUtils究竟是如何封装的,怎么会比原生反射快这么多?
小菜百思不得其解于是决定查看源码进行分析原因
打开 ReflectionUtils ,发现其有非常多的方法和字段,其中重要的两个字段:
private static final MapClass, Method[] declaredMethodsCache = new ConcurrentReferenceHashMap(256);private static final MapClass, Field[] declaredFieldsCache = new ConcurrentReferenceHashMap(256);这两个字段是缓存,declaredMethodsCache分别存储Class对象以及对应的方法数组,而declaredFieldsCache存储Class对象和对应的字段数组
小菜心想:难道ReflectionUtils是通过缓存来加快速度的?难道反射通过Class获取方法数组和字段数组的用时很长?
剩下的方法看不出个所以然,于是小菜决定从案例中的方法对比进行查看:
getConstructor小菜先从原生API获取构造器的方法入手
public Constructor getConstructor(Class... parameterTypes) throws NoSuchMethodException, SecurityException { //安全管理器检查访问权限 checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), true); //获取构造器 return getConstructor0(parameterTypes, Member.PUBLIC);}在checkMemberAccess方法中会获取安全管理器检查是否允许访问,但默认情况下是没有安全管理器的
接着查看getConstructor0方法:
private Constructor getConstructor0(Class[] parameterTypes, int which) throws NoSuchMethodException{ //会先获取构造器数组 Constructor[] constructors = privateGetDeclaredConstructors((which == Member.PUBLIC)); for (Constructor constructor : constructors) { //遍历找到参数符合条件的构造器 if (arrayContentsEq(parameterTypes, constructor.getParameterTypes())) { //通过工厂copy对象返回 return getReflectionFactory().copyConstructor(constructor); } } throw new NoSuchMethodException(getName() + ".init" + argumentTypesToString(parameterTypes));}会先获取构造器数组 privateGetDeclaredConstructors遍历找到参数符合条件的构造器 arrayContentsEq通过工厂copy对象返回 copyConstructor主要查看privateGetDeclaredConstructors 获取构造器数组的流程:
private Constructor[] privateGetDeclaredConstructors(boolean publicOnly) { //检查初始化 checkInitted(); Constructor[] res; //获取反射数据 ReflectionData 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[] temporaryRes = (Constructor[]) 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字段可以发现,不止构造器使用缓存,不同访问权限的字段和方法也会使用缓存
private static class ReflectionData { volatile Field[] declaredFields; volatile Field[] publicFields; volatile Method[] declaredMethods; volatile Method[] publicMethods; volatile Constructor[] declaredConstructors; volatile Constructor[] 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是如何获取构造器的
public static Constructor accessibleConstructor(Class clazz, Class... parameterTypes) throws NoSuchMethodException { //调用原生获取构造器 Constructor ctor = clazz.getDeclaredConstructor(parameterTypes); //设置允许访问 makeAccessible(ctor); return ctor;}调用原生API获取构造器,只是访问权限为 DECLARED 而不是 public担心访问权限不足,设置允许访问通过获取构造器的方法进行比较,小菜认为ReflectionUtils的API反而多了一步makeAccessible,会更耗时
于是进行只获取构造器的测试:
调用方法次数11_00010_0001_000_00010_000_000原生耗时ms1241776ReflectionUtils耗时ms421344251由此可以看出ReflectionUtils带来的性能提升并不是在获取构造器上,那只能是“问题”出在方法Method和字段Field上了
getDeclaredMethod继续查看原生API获取方法的源码:
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;}通过安全管理器检查是否允许访问 checkMemberAccess (前面已经说过)通过缓存获取方法数组 privateGetDeclaredMethods(类似构造器,都是使用ReflectionData做缓存)查找方法 searchMethods继续查看searchMethods流程与构造器类似:
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));}遍历查找方法找到方法后通过工厂创建对象返回总结一下可能耗时的操作ReflectionData缓存中不存在 (第一次获取方法数组会去调用本地方法获取)遍历查找方法 (如果方法太多可能开销大)通过工厂copy创建对象返回(临时、复杂对象创建的开销)ReflectionUtils.findMethod再来查看ReflectionUtils的API查找方法与原生有什么区别
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;}获取方法数组,如果是接口调用getMethods(会去调原生API并copy),否则调用getDeclaredMethods循环查找比较,找到后返回,找不到找父类小菜心想:我还以为会在循环上做文章呢,结果也是循环查找,复杂度与方法数量有关
继续查看getDeclaredMethods:
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(); //获取接口中的方法 ListMethod 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();}查询缓存,有结果直接返回没有结果,调用原生API查询并合并接口中的方法,处理结果后放入缓存经过小菜的细心比较:找到方法后原生API总是用工厂去创建getReflectionFactory().copyMethod(res),而ReflectionUtils会调用原生方法getDeclaredMethods提前把方法数组创建好放到缓存中,后续找到直接返回
小菜继续向下翻看源码,但是发现 ReflectionUtils 调用方法的API也是去调用原生的,没有区别
小菜继续查看获取字段以及设置相关的源码,发现与方法类似
小菜心想:难道每次多创建复杂对象竟然会造成这么大的开销?难道是频繁创建对象导致gc?
突然小菜认为是JVM参数未设置,突然增加这么多对象,肯定是会堆扩容和GC的
小菜后续又试了一下千万次循环的数据有下降,但是差不多只有几十毫秒影响不大
不甘心的小菜又重读了一遍源码,最后发现原生反射的缓存ReflectionData是软引用
这就说明当gc发生,缓存会被清空,导致需要重新加载从而影响性能
private ReflectionData reflectionData() { //软引用 SoftReferenceReflectionData reflectionData = this.reflectionData; int classRedefinedCount = this.classRedefinedCount; ReflectionData 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~
总结1.反射是需要检查访问权限的,比如说私有字段是否允许访问
2.使用反射进行方法调用时通常是Object,因此会涉及到需要强制类型转换
3.JIT即时编译器会将循环次数多的热点代码进行编译成本地码,而后续不再需要解释执行,从而进行优化
4.反射需要运行时动态解析类的元数据并查找,动态解析导致可能无法使用JIT
5.为了安全,反射调用本地方法查找方法、字段数组时,通常会将对象进行copy后返回新的实例
6.原生反射使用软引用作为缓存,虽然适合内存弹性伸缩,但是gc时会导致缓存丢失需要重新加载,而ReflectionUtils的缓存是强引用不会因为gc而丢失
点击关注公众号,“技术干货”及时达!
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线