My Profile Photo

庄津津的技术博客


众里寻他千百度,蓦然回首,那人却在灯火阑珊处


Java反射机制


我们知道反射是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