ReflectionUtils提高反射性能!
有一次小菜遇上一个通用的需求,于是决定在项目中使用反射,等到小菜提交代码后,审核代码的技术leader直摇头,又把小菜给叫过去了
技术leader:小菜同学,项目里用反射性能是会变慢的,但有时候为了通用性是可以用反射的,原生的反射API性能没那么好,我们可以使用Spring框架封装的ReflectionUtils
工具类
小菜嘀嘀咕咕的走回工位:这个老登儿,上次就让我改成BigDecimal
,这次又要我改成ReflectionUtils
算了,工欲善其事,必先利其器
,让我先来看看这个ReflectionUtils
到底快多少
测试性能
先写下一个实体类(省略方法),通过反射来创建实例,并通过反射修改字段的数据
代码语言:java
复制
代码语言:javascript复制public class ReflectionObject {
private String name;
private int age;
}
先写下原生反射的代码:
- 先使用构造器创建实例
- 再通过Method调用方法修改字段数据
- 直接修改字段数据
代码语言:java
复制
代码语言:javascript复制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
复制
代码语言:javascript复制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
复制
代码语言:javascript复制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
复制
代码语言:javascript复制public Constructor<T> getConstructor(Class<?>... parameterTypes)
throws NoSuchMethodException, SecurityException {
//安全管理器检查访问权限
checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), true);
//获取构造器
return getConstructor0(parameterTypes, Member.PUBLIC);
}
在checkMemberAccess方法中会获取安全管理器检查是否允许访问,但默认情况下是没有安全管理器的
接着查看getConstructor0方法:
代码语言:java
复制
代码语言:javascript复制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));
}
- 会先获取构造器数组 privateGetDeclaredConstructors
- 遍历找到参数符合条件的构造器 arrayContentsEq
- 通过工厂copy对象返回 copyConstructor
主要查看privateGetDeclaredConstructors 获取构造器数组的流程:
代码语言:java
复制
代码语言:javascript复制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
复制
代码语言:javascript复制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
复制
代码语言:javascript复制public static <T> Constructor<T> accessibleConstructor(Class<T> clazz, Class<?>... parameterTypes)
throws NoSuchMethodException {
//调用原生获取构造器
Constructor<T> ctor = clazz.getDeclaredConstructor(parameterTypes);
//设置允许访问
makeAccessible(ctor);
return ctor;
}
- 调用原生API获取构造器,只是访问权限为
DECLARED
而不是 public - 担心访问权限不足,设置允许访问
通过获取构造器的方法进行比较,小菜认为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
复制
代码语言:javascript复制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流程与构造器类似:
代码语言:java
复制
代码语言:javascript复制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查找方法与原生有什么区别
代码语言:java
复制
代码语言:javascript复制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:
代码语言:java
复制
代码语言:javascript复制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();
}
- 查询缓存,有结果直接返回
- 没有结果,调用原生API查询并合并接口中的方法,处理结果后放入缓存
经过小菜的细心比较:找到方法后原生API总是用工厂去创建getReflectionFactory().copyMethod(res)
,而ReflectionUtils会调用原生方法getDeclaredMethods
提前把方法数组创建好放到缓存中,后续找到直接返回
小菜继续向下翻看源码,但是发现 ReflectionUtils
调用方法的API也是去调用原生的,没有区别
小菜继续查看获取字段以及设置相关的源码,发现与方法类似
小菜心想:难道每次多创建复杂对象竟然会造成这么大的开销?难道是频繁创建对象导致gc?
突然小菜认为是JVM参数未设置,突然增加这么多对象,肯定是会堆扩容和GC的
小菜后续又试了一下千万次循环的数据有下降,但是差不多只有几十毫秒影响不大
不甘心的小菜又重读了一遍源码,最后发现原生反射的缓存ReflectionData是软引用
这就说明当gc发生,缓存会被清空,导致需要重新加载从而影响性能
代码语言:java
复制
代码语言:javascript复制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