Java反射的性能奥秘

在多年以前开发DB中间件时,我曾经专门测试研究过Java反射的性能损耗问题,最终结论是Reflect带来的性能损耗对于基础组件来说不可忽视,因此最终采用Javassist的动态字节码方案解决这个性能损耗。

最近在研发另一款基础组件时,突发奇想地打算放弃动态字节码,直接使用Reflect硬刚一波。

直接调用与反射调用

以下是非常简单的JMH测试代码,被测试的方法now直接返回当前时间戳:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ReflectBenchmark {
private static final ReflectBenchmark B = new ReflectBenchmark();
@Benchmark
public void direct() {
long ms = B.now();
}
@Benchmark
public void reflect() throws Exception {
Object ms = ReflectBenchmark.class.getMethod("now").invoke(B);
}
public long now() {
return System.currentTimeMillis();
}
}

最终测试数据如下:

1
2
3
Benchmark                         Mode  Cnt  Score    Error  Units
ReflectBenchmark.direct avgt 9 0.026 ± 0.001 us/op
ReflectBenchmark.reflect avgt 9 0.147 ± 0.002 us/op

可以认为direct的性能指标等于System.currentTimeMillis(),从以上数据来看反射消耗了大概0.12us,这个耗时差不多等于普通JSON的反序列化了,不可接受。

优化 - Method缓存

Class的源代码来看,其getMethod是一个相对较复杂的方法,尝试直接缓存Method进行性能测试:

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 class ReflectBenchmark {
private static final ReflectBenchmark B = new ReflectBenchmark();
private static final Method method;
static {
try {
method = ReflectBenchmark.class.getMethod("now");
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}
@Benchmark
public void direct() {
long ms = B.now();
}
@Benchmark
public void reflect() throws Exception {
Object ms = ReflectBenchmark.class.getMethod("now").invoke(B);
}
@Benchmark
public void reflectMethod() throws Exception {
Object ms = method.invoke(B);
}
public long now() {
return System.currentTimeMillis();
}
}

最终测试数据如下:

1
2
3
4
Benchmark                         Mode  Cnt  Score    Error  Units
ReflectBenchmark.direct avgt 9 0.026 ± 0.001 us/op
ReflectBenchmark.reflect avgt 9 0.147 ± 0.002 us/op
ReflectBenchmark.reflectMethod avgt 9 0.028 ± 0.001 us/op

效果不错!缓存Method之后,直接进行invoke相对于直接调用仍有一点性能损耗,但是已经解决掉了反射的绝大部分性能问题。

优化 - MethodAccessor

分析Methodinvoke源代码可以发现,其内部实现完全依赖于MethodAccessor,既然如此直接一步到位缓存它不就好了吗?说干就干:

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 class ReflectBenchmark {

private static final ReflectBenchmark B = new ReflectBenchmark();
private static final Method method;
private static final MethodAccessor accessor;

static {
try {
method = ReflectBenchmark.class.getMethod("now");
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
accessor = ReflectionFactory.getReflectionFactory().newMethodAccessor(method);
}

// ...

@Benchmark
public void reflectAccessor() throws Exception {
Object ms = accessor.invoke(B, null);
}

public long now() {
return System.currentTimeMillis();
}
}

果然不出所料,最终反射性能“完全”等于方法直接调用,起码JMH不能测出它的性能差距:

1
2
3
4
5
Benchmark                         Mode  Cnt  Score    Error  Units
ReflectBenchmark.direct avgt 9 0.026 ± 0.001 us/op
ReflectBenchmark.reflect avgt 9 0.147 ± 0.002 us/op
ReflectBenchmark.reflectMethod avgt 9 0.028 ± 0.001 us/op
ReflectBenchmark.reflectAccessor avgt 9 0.026 ± 0.001 us/op

对比Cglib

第三方库如Cglib等,也通过动态字节码提供了类似于MethodAccessor的功能,如FastMethod。将其套入现有的Benchmark的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ReflectBenchmark {

private static final ReflectBenchmark B = new ReflectBenchmark();
private static final Method method;
private static final MethodAccessor accessor;

private static final FastClass cglibClass;
private static final FastMethod cglibMethod;

static {
// ...
cglibClass = FastClass.create(B.getClass());
cglibMethod = cglibClass.getMethod(method);
}

// ...

@Benchmark
public void reflectCglib() throws Exception {
Object ms = cglibMethod.invoke(B, null);
}

}

为了更加方便地观测性能指标,本轮测试采用ns/op作为单位:

1
2
3
4
5
6
Benchmark                         Mode  Cnt    Score   Error  Units
ReflectBenchmark.direct avgt 9 25.497 ± 0.170 ns/op
ReflectBenchmark.reflect avgt 9 149.775 ± 1.556 ns/op
ReflectBenchmark.reflectAccessor avgt 9 26.033 ± 0.561 ns/op
ReflectBenchmark.reflectCglib avgt 9 28.733 ± 1.328 ns/op
ReflectBenchmark.reflectMethod avgt 9 28.446 ± 0.715 ns/op

看起来cglib的性能似乎比缓存了Method的直接反射invoke稍微慢了一丁点,这个结果比较令人意外。整体而言,仍然是MethodAccessor最快,几乎没有性能损耗。细究的话,它可能比普通调用多了1~2次cpu运算。

结论

借助于MethodAccessor,应该可以实现无性能损耗的反射调用。长期以来,许多概念中的反射性能问题主要集中在ClassMethodField属性的提取,单纯的方法调用在jvm层面应该相当于不同指针指向同一块方法字节码区,并不存在性能损耗。

具体MethodInvoke原理,可以参考这篇文章:https://www.cnblogs.com/onlywujun/p/3519037.html

本文涉及到的Benchmark代码托管在这个GitHub仓库中。