反射是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()方法。
接下来看下获取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 反射调用的性能开销进行分析。
反射功能使用
获取Class 对象
- 使用静态方法Class.forName
- 调用对象的getClass() 方法
- 直接类名 + “.class” 访问
[1]https://www.jianshu.com/p/4b98cd4af198