我们知道反射是Java中比较常用的一种技术。就比如JAVA IDE工具里面的代码补全功能,就是用的反射,当你打一个对象.,然后会通过反射这个类的getMethods和getFields拿到信息并展示出来。Java调试工具亦是如此。很多我们用过的Web框架为了更好的扩展性也是运用了Java反射技术,Spring中的也用到大量的反射的地方,比如控制反转(IOC)。
反射的性能比较
但是经常听说Java反射性能很低,要慎用。那为何反射性能那么低,我们来一探究竟,首先我们跑20亿次直接调用和20亿次反射调用做对比,每1亿次记录一下耗时,我们取最后5次耗时做参考,考虑到这是预热后的峰值数据。当然还可以使用JMH去做基准测试,我们这儿就不展开介绍。
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
public static void target(int i) {
}
private static void directInvoke() {
System.out.println("=========directInvoke=========");
long currentTime = System.currentTimeMillis();
for (int i = 0; i < 2000_000_000; i++) {
if (i > 0 && i % 100_000_000 == 0) {
System.out.println((System.currentTimeMillis() - currentTime) + "ms");
currentTime = System.currentTimeMillis();
}
doSomething(128);
}
}
private static void refectInvoke() throws Exception {
System.out.println("=========refectInvoke=========");
Method method = MethodDemo.class.getMethod("doSomething", int.class);
long currentTime = System.currentTimeMillis();
for (int i = 0; i < 2000_000_000; i++) {
if (i > 0 && i % 100_000_000 == 0) {
System.out.println((System.currentTimeMillis() - currentTime) + "ms");
currentTime = System.currentTimeMillis();
}
method.invoke(null, 128);
}
}
public static void main(String[] args) throws Exception {
directInvoke();
refectInvoke();
}
运行结果我们可以以直接调用作为基准数据,我们看到直接调用的平均耗时差不多是120ms,而反射调用耗时差不多400ms左右,可以看到反射调用耗时差不多基准的3.3倍。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
=========directInvoke=========
...
113ms
109ms
109ms
109ms
116ms
=========refectInvoke=========
...
400ms
387ms
407ms
398ms
414ms
反射的类
- Method
- Constructor
- Field
Method的调用
我们平常用Method.invoke方法去反射调用一个方法。其实是调用MethodAccessor.invoke方法,MethodAccessor是一个接口,它有两个具体实现,一个是委派实现DelegatingMethodAccessorImpl,一个是本地代码实现NativeMethodAccessorImpl。这里采用装饰器模式和委派模式设计,是个有意思的设计。其中被委派类有两种实现,一种是本地代码实现,一种动态字节码生成实现。其中动态字节码调用和本地代码调用相比,其运行效率要比后者快将近40,50倍(笔者环境是mac+jdk8)。但是如果只是调用一次的话,动态字节码调用会比本地代码调用慢3,4倍(因为动态生成字节码比较耗时,这个数字是从郑雨迪老师的文章中看到的)。这也是java默认情况下,为什么先使用本地代码调用,等调用次数超出阈值时,切换成动态字节码调用。
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
package java.lang.reflect;
...
public final class Method extends Executable {
...
public Object invoke(Object obj, Object... args) {
...
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}
private MethodAccessor acquireMethodAccessor() {
MethodAccessor tmp = null;
if (root != null) tmp = root.getMethodAccessor();
if (tmp != null) {
methodAccessor = tmp;
} else {
//这里创建MethodAccessor的委派实现
tmp = reflectionFactory.newMethodAccessor(this);
setMethodAccessor(tmp);
}
return tmp;
}
...
}
这里我们来看下创建委派实现的过程,当然Java有提供参数配置,当配置-Dsun.reflect.noInflation=true
,那第一次调用就是动态字节码调用。你也可以根据场景配置阈值为n次-Dsun.reflect.inflationThreshold=n
,这样子当超过n次调用本地代码实现后,就会走动态字节码调用。
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package sun.reflect;
...
public class ReflectionFactory {
...
public MethodAccessor newMethodAccessor(Method method) {
//检查并初始化初始化一些参数,如noInflation,inflationThreshold等等
checkInitted();
if (noInflation && !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) {
//当设置noInflation为true,且这个方法的声明类不是一个匿名类时,利用asm技术生成动态字节码对象
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;
}
}
private static void checkInitted() {
...
AccessorController.doPrivileged(
new PrivilegedAction<Void>() {
public void run() {
...
//读取配置-Dsun.reflect.noInflation
String val = System.getProperty("sun.reflect.noInflation");
if (val != null && val.equals("true")) {
noInflation = true;
}
//读取配置-Dsun.reflect.inflationThreshold
val = System.getProperty("sun.reflect.inflationThreshold");
if (val != null) {
try {
inflationThreshold = Integer.parseInt(val);
} catch (NumberFormatException e) {
...
}
}
...
}
}
);
}
...
}
如果说默认情况下,我们其实是委派实现,那么被委派对象是怎么从本地代码实现变成动态字节码实现
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
package sun.reflect;
...
public class NativeMethodAccessorImpl extends MethodAccessorImpl {
...
public Object invoke(Object obj, Object[] args)
throws IllegalArgumentException, InvocationTargetException {
if (++numInvocations > ReflectionFactory.inflationThreshold()
&& !ReflectUtil.isVMAnonymousClass(method.getDeclaringClass())) {
// 每次调用次数加1
// 判断调用次数超过阈值并且非匿名类,利用asm技术生成动态字节码对象
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);
}
...
}
改进
我们将代码128中i改成127看看
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void target(int i) {
}
private static void directInvoke() {
...
doSomething(127);
}
private static void refectInvoke() throws Exception {
...
method.invoke(null, 127);
}
public static void main(String[] args) throws Exception {
directInvoke();
refectInvoke();
}
得出结果,发现反射调用这边性能有很大的提升,可以看到反射调用耗时差不多基准的2.25倍
1
2
3
4
5
6
7
=========refectInvoke=========
...
278ms
273ms
278ms
273ms
283ms
那么这里面的性能差在哪里,原因在于method.invoke的第二个参数是Object…,正常127传进去,java会编译成Integer.valueOf(127),这个可以用javap查看一下。然而Integer.valueOf有缓存对象-128~127,因此调用Integer.valueOf(127)不会产生额外的对象,而Integer.valueOf(128)会产生新的对象,这样可能占用堆内存,使得GC更加频繁。
1
2
3
4
5
6
7
8
9
10
11
12
92: aload_0
93: aconst_null
94: iconst_1
95: anewarray #23 // class java/lang/Object
98: dup
99: iconst_0
100: bipush 127
102: invokestatic #24 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
105: aastore
106: invokevirtual #25 // Method java/lang/reflect/Method.invoke:(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;
109: pop
那么我们可不可以再进一步提高性能呢,我们看到每次调用invoke的时候,都会判断权限,我们只要提前设置好可访问,这样子就可以节省一部分开销,修改代码如下
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
public static void target(int i) {
}
private static void directInvoke() {
...
doSomething(127);
}
private static void refectInvoke() throws Exception {
System.out.println("=========refectInvoke=========");
Method method = MethodDemo.class.getMethod("doSomething", int.class);
method.setAccessible(true);
long currentTime = System.currentTimeMillis();
for (int i = 0; i < 2000_000_000; i++) {
if (i > 0 && i % 100_000_000 == 0) {
System.out.println((System.currentTimeMillis() - currentTime) + "ms");
currentTime = System.currentTimeMillis();
}
method.invoke(null, 127);
}
}
public static void main(String[] args) throws Exception {
directInvoke();
refectInvoke();
}
得出结果,可以看到反射调用耗时差不多基准的2倍
1
2
3
4
5
6
7
=========refectInvoke=========
...
244ms
236ms
241ms
236ms
243ms