让我们从最基本的反射开始, 了解反射的调用过程, 再来看Java 7中新加入的 MethodHandle 如何提高反射的运行效率。
谈一谈Java反射 (上)
小到Minecraft的注解框架, 大到Spring实例化其Beans, 我们难免都会接触到Java一个非常重要的功能 - 反射 ( Reflection ) , 但提到反射, 伴随而来的往往还有三个字 - 效 率 差 应该在实际开发中避免使用。 那么反射究竟在哪里慢 ?
让我们从最基本的反射开始, 了解反射的调用过程, 再来看Java 7中新加入的 MethodHandle 如何提高反射的运行效率。
注意: 本文采用 Oracle JDK 13.01
从 getMethod 和 getDeclaredMethod 说起
在使用反射的过程中, 我们往往会用到 getMethod 和 getDeclaredMethod 这两个方法, 用来获取类中方法的 Method 对象
在 JDK 13 中, 这两个方法的代码如下:
1 |
|
通过源代码, 我们可以看出, 两种方法的逻辑基本一致, 都是检查方法权限, 通过 getMethod0 或者是 privateGetDeclaredMethods 获取 Method 对象, 然后再返回 Method 对象的拷贝
在检查方法权限的部分, 我们可以看出, 在 getMethod 方法下, 传入的参数为 Member.PUBLIC , 而在 getDeclaredMethod 中传入的则是 Member.DECLARED , 查阅源代码我们可以知道, 两个量均为定义在 Member 接口中的整型常量, 其中 PUBLIC 包括该类(或接口)所有的访问权限为 public 的方法也包括继承的成员, 而 DECLARED 则是该类所有声明的成员, 包括 public , protected , private 的成员, 但不包括继承的成员。具体代码及文档如下:
1 | public interface Member { |
继续深入 - getMethod0 方法
以下是 getMethod0 的代码, 通过注释我们可以知道, getMethod0 的返回值是一个 root 的 Method 对象, 并且强调这个 root 对象不应该暴露到外部, 而应该通过 ReflectionFactory.copyMethod 拷贝
1 | // Returns a "root" Method object. This Method object must NOT |
看完注释, 再来看源代码, 这里通过 getMethodsRecursive 方法获取到了一个 MethodList 对象, 并通过其 getMostSpecific 方法, 筛选出最为明确具体的方法返回
注: 或者说当一个方法签名子类和父类同时满足要求时, 优先选择子类而不是父类
再进一步 - getMethodsRecursive 方法
以下为 getMethodsRecursive 的源代码, 可以看出注释同样是强调该方法返回的 root Method 对象不应该暴露于外部, 而应该通过 ReflectionFactory.copyMethod 拷贝
不过有趣的一点是, 在这里我们看到了在 getDeclaredMethod 方法中出现的 privateGetDeclaredMethods 方法, 不过不同的是, 这里的 publicOnly 参数传入的是 true 而非 false
1 | // Returns a list of "root" Method objects. These Method objects must NOT |
通过阅读源代码我们可以发现, 这里获取方法分为三个步骤:
- 通过privateGetDeclaredMethods获取自己所有的 public方法, 并通过MethodList#filter
方法过滤方法, 找出所有满足条件的方法 , 如果至少有一个满足条件的方法, 便不再继续搜索而是直接返回, 因为它肯定 override 了父类或者接口的对应方法 - 如果没有在自己的方法中找到, 便去递归 搜索父类的的方法
- 如果没有在自己的方法中找到, 便去递归搜索自己所有接口的方法
观察上述三个步骤我们可以发现, 最终获取方法, 我们还是要通过 privateGetDeclaredMethods 方法, 所以接下来, 让我们去深入了解一下它的实现
走到底了 - privateGetDeclaredMethods 方法
1 | // Returns an array of "root" methods. These Method objects must NOT |
终于, 我们来到了 getMethod 和 getDeclaredMethod 方法的终点, 观察他的方法, 似乎也很简单, 如果存在缓存则直接使用缓存获取 Method 对象的数组, 如果不存在缓存,就从 JVM 获取并将其存入缓存
那么, 缓存对象的 ReflectionData
1 | /** |
可以看到, ReflectionData 类中缓存了Class中的所有属性和方法以及构造函数, 甚至是类的 simpleName 和 canonicalName
让我们再来看看 reflectionData 方法
1 | // Lazily create and cache ReflectionData |
在 Class 对象中, 存在一个 ReflectionData 的软引用作为缓存, 如果缓存为空, 或者软引用已经被GC , 或者缓存已经过期, 就使用 newReflectionData 方法重建缓存, 并替换掉旧的缓存
1 | private ReflectionData<T> newReflectionData(SoftReference<ReflectionData<T>> oldReflectionData, |
至此, getMethod 部分的代码已经结束
回过头来提一提 - searchMethod 方法
1 | // This method does not copy the returned Method object! |
可以看出, 这个方法其实相当简单明了, 判断方法的签名是与给定的相同
再来看一看- getReflectionFactory()#copyMethod() 方法
方法的注释中反复提到, 得到的 root Method 对象不能暴露给外界, 而需要通过 ** getReflectionFactory()#copyMethod() ** 方法拷贝
1 | /** Makes a copy of the passed method. The returned method is a |
根据注释,实际上最后调用的是Method#copy方法
1 | ** |
可以看出, 该方法只能拷贝 root Method 对象, 并且在实例化了拷贝后的 Method 对象后, 将其 root 属性设置为当前的 root Method 对象
同时, 根据注释以及源代码, 所有的指向同一个底层方法的 Method 对象都会共用一个 MethodAcessor 对象,
到这里看起来, 似乎在Java反射的获取 Method 对象环节并没有什么特别明显的效率开销的问题, 那么, 接下来我们再来看看, 方法是怎样被 invoke 的
Method#invoke 开始了, 从方法调用开始
1 | /** |
可以看到, invoke 方法的实现分为上个步骤
检查是否有调用权限
获取 MethodAccessor 对象
调用 MethodAccessor#invoke 对方法进行调用
Step 1. 检查调用权限
如果override 属性为true 则跳过检查, 即调用Method#setAccessible(true) 为设置 override 属性为 true
Step 2.获取 MethodAccessor 对象
获取方法的 MethodAccessor 如果为空, 则调用 acquireMethodAccessor 方法获取 MethodAccessor
acquireMethodAccessor 方法的源代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// NOTE that there is no synchronization used here. It is correct
// (though not efficient) to generate more than one MethodAccessor
// for a given Method. However, avoiding synchronization will
// probably make the implementation more scalable.
private MethodAccessor acquireMethodAccessor() {
// First check to see if one has been created yet, and take it
// if so
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;
} 可以看到, 这里的方法并没有 同步化 ( synchronization ) , 注释也解释说, 为给定的方法,生成多于一个的 MethodAccessor 对象是没有问题的, 尽管它可能会不够高效, 但避免同步化, 也使得这个实现更加灵活
通过源代码我们可以看到, 他会尝试先获取 root Method 对象的 MethodAccessor 对象, 如果其为空, 则通过 ReflectionFactory#newMethodAccessor 方法新建一个 MethodAccessor 对象, 并将其设置为当前 Method 的 MethodAccessor
下面是 ReflectionFactory#newMethodAccessor 的源代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33public MethodAccessor newMethodAccessor(Method method) {
checkInitted();
if (Reflection.isCallerSensitive(method)) {
Method altMethod = findMethodForReflection(method);
if (altMethod != null) {
method = altMethod;
}
}
// use the root Method that will not cache caller class
Method root = langReflectAccess().getRoot(method);
if (root != null) {
method = root;
}
if (noInflation && !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) {
return new MethodAccessorGenerator().
generateMethod(method.getDeclaringClass(),
method.getName(),
method.getParameterTypes(),
method.getReturnType(),
method.getExceptionTypes(),
method.getModifiers());
} else {
NativeMethodAccessorImpl acc =
new NativeMethodAccessorImpl(method);
DelegatingMethodAccessorImpl res =
new DelegatingMethodAccessorImpl(acc);
acc.setParent(res);
return res;
}
} 我们可以看到, 该方法会先调用 ReflectionFactory#checkInitted 方法, 检查 java.lang.reflect.Method 的 static initializer 是否已经完成初始化
以下是 ReflectionFactory#checkInitted 方法的源代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35/** We have to defer full initialization of this class until after
the static initializer is run since java.lang.reflect.Method's
static initializer (more properly, that for
java.lang.reflect.AccessibleObject) causes this class's to be
run, before the system properties are set up. */
private static void checkInitted() {
if (initted) return;
// Defer initialization until module system is initialized so as
// to avoid inflation and spinning bytecode in unnamed modules
// during early startup.
if (!VM.isModuleSystemInited()) {
return;
}
Properties props = GetPropertyAction.privilegedGetProperties();
String val = props.getProperty("sun.reflect.noInflation");
if (val != null && val.equals("true")) {
noInflation = true;
}
val = props.getProperty("sun.reflect.inflationThreshold");
if (val != null) {
try {
inflationThreshold = Integer.parseInt(val);
} catch (NumberFormatException e) {
throw new RuntimeException("Unable to parse property sun.reflect.inflationThreshold", e);
}
}
disableSerialConstructorChecks =
"true".equals(props.getProperty("jdk.disableSerialConstructorChecks"));
initted = true;
} 可以看到, 其中设置了在 ** ReflectionFactory#newMethodAccessor ** 方法中使用到的 noInflation 属性
对于 sun.reflect.noInflation 属性, Oracle的Blog上是这么解释的
sun.reflect.noInflation
This boolean will disable inflation (the default use of JNI before the threshold is reached). In other words, if this is set to true, we immediately skip to generating a pure-Java implementation on the first access. (default: false)
从代码中我们可以看到, 当 noInflation 属性直接设置为 true 会直接采用纯Java版本的MethodAcessorImpl 即 MagicAccessorImpl , 去生成 Java Bytecode
默认情况下我们会采用 NativeMethodAcessorImpl 以及采用 DelegatingMethodAccessorImpl 做代理的方式实现
以下是 NativeMethodAcessorImpl 以及 DelegatingMethodAccessorImpl 的源代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40/** Used only for the first few invocations of a Method; afterward,
switches to bytecode-based implementation */
class NativeMethodAccessorImpl extends MethodAccessorImpl {
private final Method method;
private DelegatingMethodAccessorImpl parent;
private int numInvocations;
NativeMethodAccessorImpl(Method method) {
this.method = method;
}
public Object invoke(Object obj, Object[] args)
throws IllegalArgumentException, InvocationTargetException
{
// We can't inflate methods belonging to vm-anonymous classes because
// that kind of class can't be referred to by name, hence can't be
// found from the generated bytecode.
if (++numInvocations > ReflectionFactory.inflationThreshold()
&& !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) {
MethodAccessorImpl acc = (MethodAccessorImpl)
new MethodAccessorGenerator().
generateMethod(method.getDeclaringClass(),
method.getName(),
method.getParameterTypes(),
method.getReturnType(),
method.getExceptionTypes(),
method.getModifiers());
parent.setDelegate(acc);
}
return invoke0(method, obj, args);
}
void setParent(DelegatingMethodAccessorImpl parent) {
this.parent = parent;
}
private static native Object invoke0(Method m, Object obj, Object[] args);
}
1 | /** Delegates its invocation to another MethodAccessorImpl and can |
通过 NativeMethodAccessorImpl 的源代码和注释我们可以知道, ** NativeMethodAccessorImpl** 仅仅只被用于低于 inflationThreshold 次的调用, 其内部维护了一个 numInvocations 的变量作为计数器, 当超过阈值时, 会采用 **MagicAccessorImpl 生成 Java Bytecode
该阈值为 ** sun.reflect.inflationThreshold** 属性
> **sun.reflect.inflationThreshold**
>
>This integer specifies the number of times a method will be accessed via the JNI implementation before a custom pure-Java accessor is generated. (default: 15)
这也是为什么会使用 DelegatingMethodAccessorImpl 作为代理
根据注释
**”Inflation” mechanism. **
Loading bytecodes to implement
Method.invoke() and Constructor.newInstance() currently costs
3-4x more than an invocation via native code for the first
invocation (though subsequent invocations have been benchmarked
to be over 20x faster). Unfortunately this cost increases
startup time for certain applications that use reflection
intensively (but only once per class) to bootstrap themselves.
To avoid this penalty we reuse the existing JVM entry points
for the first few invocations of Methods and Constructors and
then switch to the bytecode-based implementations.
Java版本的MagicAcessorImpl 的调用效率是 NativeMethodAcessorImpl 的 20 倍以上, 但初次调用生成的是否会比 Native 版本的慢 3 - 4倍, 这带来了更长的启动时间
这也是为什么 Java 反射会采用这种策略
调用 MethodAccessor#invoke 对方法进行调用
在我们得到了 MethodAccessor 对象后, 就可以通过其 invoke 方法实现最终的调用操作
NativeMethodAccessorImpl 的 invoke 方法为一个计数器加调用一个名为 invoke0 的 native 方法
即
1 | public Object invoke(Object obj, Object[] args) |
DelegatingMethodAccessorImpl 的 invoke 方法更不用说, 直接调用代理对象的 invoke 方法
即
1 | public Object invoke(Object obj, Object[] args) |
而 MagicAcessorImpl 通过 MethodAccessorGenerator#generate 方法生成字节码之后, 会调用MethodAcessorGenerator#emitInvoke 方法, 为invoke 和 newInstance 方法生成实际的调用
其源代码如下
1 | /** This emits the code for either invoke() or newInstance() */ |
通过这段代码的注释, 我们可以发现, 生成的 **invoke** 方法对传入的参数进行了校验工作, 并尝试了所有可能的扩展操作
至此, Java反射的流程已经介绍完毕
慢在哪里?
反射需要查找类的方法表, 需要对方法进行遍历操作
反射调用时, Method#invoke 方法和 emitInvoke 方法会对传入的参数进行校验, 其中会进行 boxing 和 unboxing 操作
每次反射调用都需要校验方法的可见性
反射调用方法难以被JIT优化, 难以被内联操作
即Oracle的文档中提到的
Drawbacks of Reflection
Reflection is powerful, but should not be used indiscriminately. If it is possible to perform an operation without using reflection, then it is preferable to avoid using it. The following concerns should be kept in mind when accessing code via reflection.
Performance Overhead
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.
Security Restrictions
Reflection requires a runtime permission which may not be present when running under a security manager. This is in an important consideration for code which has to run in a restricted security context, such as in an Applet.
Exposure of Internals
Since reflection allows code to perform operations that would be illegal in non-reflective code, such as accessing private fields and methods, the use of reflection can result in unexpected side-effects, which may render code dysfunctional and may destroy portability. Reflective code breaks abstractions and therefore may change behavior with upgrades of the platform.
结语
本篇文章, 从 getMethod 方法说起到 invoke 过了一遍 Java 的方法调用反射, 简单分析了 传统的反射为什么效率低下
而下一篇文章, 将介绍于 JDK7 引入的 MethodHandle , 并介绍其如何优化反射调用