反射是Java 中非常重要的特性,它允许正在运行的Java程序观测,甚至是修改程序的动态行为。
例如:我们可以通过Class 对象枚举出该类所有方法,我们还可以通过Method.Accessible 绕过Java 语言的访问权限,在私有的方法所在类之外的地方调用该方法。
在Java 开发环境(IDE)中当我们输入对象后输入点号时,编译器会根据点号前的数据动态的展示出对象中的属性和方法。
在Web开发中,我们经常使用的各种通用框架为了保证框架的可扩展性,往往都使用Java反射功能,根据配置文件中的信息来动态的加载不同的类,还可以为类中的属性赋值等等。例如Spring的IOC功能,底层原理就是使用反射机制。
虽然反射机制很灵活,但是在性能上却带来了一些开销,接下来我们就了解一下反射机制,并看看为什么会有性能问题。
原理
我们根据方法的反射调用来分析下源码,看看Method.invoke是如何实现的。
@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);
}
通过源码可以看到其实invoke方法实际上是委派给了MethodAccessor来处理,MethodAccessor是一个接口,有两个具体实现方法(methodAccessorImpl 是一个抽象的实现方法另外两个实现对象继承此对象),委托实现和本地实现。从代码中可以看到第一次调用时本地methodAccessor是空,所以会调用acquireMethodAccessor()方法。
image.png
接下来看下获取MethodAccessor实现方法,首先检查是否已经创建,如果创建了就使用创建的,如果没有创建就调用工厂方法创建一个
private MethodAccessor acquireMethodAccessor() {
// 首先检查是否已经创建了实现,如果创建了就使用创建的,如果没有就地要用工厂方法创建一个
MethodAccessor tmp = null;
if (root != null) tmp = root.getMethodAccessor();
if (tmp != null) {
methodAccessor = tmp;
} else {
// Otherwise fabricate one and propagate it up to the root
tmp = reflectionFactory.newMethodAccessor(this);
setMethodAccessor(tmp);
}
return tmp;
}
看下反射工厂的newMethodAccessor 方法,从下面可以看到先是检查初始化,然后判断是否开启动态代理实现,如果开启了就会使用动态实现方式(直接生成字节码方式),如果没有开启就会生成一个委派实现,委派实现的具体实现是使用本地实现来完成。
public MethodAccessor newMethodAccessor(Method var1) {
checkInitted();
if (noInflation && !ReflectUtil.isVMAnonymousClass(var1.getDeclaringClass())) {
return (new MethodAccessorGenerator()).generateMethod(var1.getDeclaringClass(), var1.getName(), var1.getParameterTypes(), var1.getReturnType(), var1.getExceptionTypes(), var1.getModifiers());
} else {
NativeMethodAccessorImpl var2 = new NativeMethodAccessorImpl(var1);
DelegatingMethodAccessorImpl var3 = new DelegatingMethodAccessorImpl(var2);
var2.setParent(var3);
return var3;
}
}
看到这里可能会有一个疑问,为什么使用委派实现穿插在中间,这是因为Java反射实现机制还有一种动态生成字节码,通过invoke指令直接调用目标的方法,委派实现是为了在动态实现和本地实现之间进行切换。
动态实现和本地实现相比,执行速度要快上20倍,这是因为动态实现直接执行字节码,不用从java到c++ 再到java 的转换,但是因为生成字节码的操作比较耗费时间,所以如果仅一次调用的话反而是本地时间快3到4倍。
为了防止很多反射调用只调用一次,java 虚拟机设置了一个阀值等于15(通过-Dsun.reflect.inflationThreshold 参数来调整),当一个反射调用次数达到15次时,委派实现的委派对象由本地实现转换为动态实现,这个过程称之为Inflation。
反射调用的Inflation机制可以通过参数(-Dsun.reflect.noInflation=true)来关闭(对应代码是newMethodAccessor 方法中的if 判断)。这样在反射调用开始的时候就会直接使用动态实现,而不会使用委派实现或者本地实现。
反射调用的性能开销
接下来我们就来看下反射调用的性能开销,在反射调用方法的例子中,我们先后调用了Class.forName,Class.getMethod,以及Method.invoke 三个操作。其中Class.forName 会调用本地方法,Class.getMethod 会遍历该类的公有方法。如果没有匹配到它还会遍历父级的公有方法,可以知道这两个操作非常耗费时间。
值得注意的是,以getMethod 方法为代表的查询操作,会返回一份查询结果的拷贝信息。因此我们避免在热点代码中使用返回Method数组的getMethods 或者getDeclareMethods方法,以减少不必要的堆空间的消耗。
在实际的开发中 ,我们通常会在应用程序中缓存Class.forName 和 Class.getMethod 的结果。因为下面我们就针对Method.invoke 反射调用的性能开销进行分析。
Java 反射效率低的原因
了解了反射的原理以后,我们来分析一下反射效率低的原因。
1. Method#invoke 方法会对参数做封装和解封操作
我们可以看到,invoke 方法的参数是 Object[] 类型,也就是说,如果方法参数是简单类型的话,需要在此转化成 Object 类型,例如 long ,在 javac compile 的时候 用了Long.valueOf() 转型,也就大量了生成了Long 的 Object, 同时 传入的参数是Object[]数值,那还需要额外封装object数组。
而在上面 MethodAccessorGenerator#emitInvoke 方法里我们看到,生成的字节码时,会把参数数组拆解开来,把参数恢复到没有被 Object[] 包装前的样子,同时还要对参数做校验,这里就涉及到了解封操作。
因此,在反射调用的时候,因为封装和解封,产生了额外的不必要的内存浪费,当调用次数达到一定量的时候,还会导致 GC。
2. 需要检查方法可见性
通过上面的源码分析,我们会发现,反射时每次调用都必须检查方法的可见性(在 Method.invoke 里)
3. 需要校验参数
反射时也必须检查每个实际参数与形式参数的类型匹配性(在NativeMethodAccessorImpl.invoke0 里或者生成的 Java 版 MethodAccessor.invoke 里);
4. 反射方法难以内联
Method#invoke 就像是个独木桥一样,各处的反射调用都要挤过去,在调用点上收集到的类型信息就会很乱,影响内联程序的判断,使得 Method.invoke() 自身难以被内联到调用方。参见 www.iteye.com/blog/rednax…
5. JIT 无法优化
在 JavaDoc 中提到:
Because reflection involves types that are dynamically resolved, certain Java virtual machine optimizations can not be performed. Consequently, reflective operations have slower performance than their non-reflective counterparts, and should be avoided in sections of code which are called frequently in performance-sensitive applications.
因为反射涉及到动态加载的类型,所以无法进行优化
反射功能使用
获取Class 对象
- 使用静态方法Class.forName
- 调用对象的getClass() 方法
- 直接类名 + “.class” 访问






还没有评论,来说两句吧...