反射(Reflection)是Java的一种机制,该种机制使得程序员有在Java程序运行时获得class的meta info(比如方法、字段表,方法签名,注解等)的能力;有许多的框架(比如大名鼎鼎的springframework)是建立在反射的基础上的。
在Oracle的官方文档中,对反射的陈述如下:Reflection enables Java code to discover information about the fields, methods and constructors of loaded classes, and to use reflected fields, methods, and constructors to operate on their underlying counterparts, within security restrictions. The API accommodates applications that need access to either the public members of a target object (based on its runtime class) or the members declared by a given class. It also allows programs to suppress default reflective access control.
在程序中使用反射带来的优势很明显:灵活地使用反射使得我们能写出抽象程度更高的代码,模块与模块之间的解耦度也将变得更高;但劣势也很明显,反射将带来性能损失。
反射的性能测试
下面两种方式的反射可能会对性能造成影响:通过反射实例化class
//一般方式Foo foo = new Foo();
//反射动态实例化类Foo foo = (Foo)Class.forName("a.b...Foo").newInstance();
2. 通过反射动态调用对象的方法
Foo foo = new Foo();
//直接调用foo.bar();
//反射调用try{
Method method = Foo.class.getMethod("bar");
method.invoke(foo);
}catch(IllegalAccessException | InvocationTargetException e){
}
这两种情况都有可能带来性能的损失,我们依然借助Java Microbenchmark Harness来测试反射对于性能的影响:
package benchmark;
import org.openjdk.jmh.annotations.*;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
@State(Scope.Thread)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
public class ReflectionBenchmark1 {
private static final int len = 1000;
private static final Method applyMethod;
private static final MethodHandle methodHandle;
static {
try {
applyMethod = BiIntegerOp.class.getMethod("apply", Integer.class, Integer.class);
methodHandle =
MethodHandles.lookup()
.findVirtual(
BiIntegerOp.class,
"apply",
MethodType.methodType(Integer.class, Integer.class, Integer.class));
} catch (Exception e) {
throw new Error(e);
}
}
private final Integer[] a;
private final BiFunction instancedObj;
private final BiFunction reflectedObj;
private int index = 0;
{
a = new Integer[len];
Random random = new Random();
for (int i = 0; i < a.length; i++) a[i] = random.nextInt(10000);
instancedObj = new BiIntegerOp();
try {
reflectedObj =
(BiIntegerOp) Class.forName(getInnerClassName(BiIntegerOp.class)).newInstance();
} catch (Exception e) {
throw new Error(e);
}
}
private static String getInnerClassName(Class> clazz) {
String className = clazz.getCanonicalName();
int index = className.lastIndexOf('.');
return className.substring(0, index) + '$' + className.substring(index + 1);
}
@Benchmark
public Integer testNormal() {
if (index >= len - 1) index = 0;
Integer i = instancedObj.apply(a[index], a[index + 1]);
index++;
return i;
}
@Benchmark
public Integer testReflectedInstance() {
if (index >= len - 1) index = 0;
Integer i = reflectedObj.apply(a[index], a[index + 1]);
index++;
return i;
}
@Benchmark
public Integer testReflectedMethod() {
if (index >= len - 1) index = 0;
try {
Integer i = (Integer) applyMethod.invoke(instancedObj, a[index], a[index + 1]);
index += 1;
return i;
} catch (IllegalAccessException | InvocationTargetException e) {
throw new IllegalArgumentException(e);
}
}
@Benchmark
public Integer testReflectedMethodAndInstance() {
if (index >= len - 1) index = 0;
try {
Integer i = (Integer) applyMethod.invoke(reflectedObj, a[index], a[index + 1]);
index += 1;
return i;
} catch (IllegalAccessException | InvocationTargetException e) {
throw new IllegalArgumentException(e);
}
}
@Benchmark
public Integer testMethodHandle() {
if (index >= len - 1) index = 0;
try {
Integer i = (Integer) methodHandle.invoke(instancedObj, a[index], a[index + 1]);
index += 1;
return i;
} catch (Throwable e) {
throw new IllegalArgumentException(e);
}
}
public static final class BiIntegerOp implements BiFunction {
@Override
public Integer apply(Integer integer, Integer integer2) {
int i1 = integer;
int i2 = integer2;
return (i1 + i2 + i1 * i2);
}
}
}
上面的测试,构造了四种情况下对对象同一个方法的调用:在new的实例上直接调用方法;
在动态生成的实例上直接调用方法;
通过反射调用new的实例上的方法;
通过反射调用动态生成的实例上的方法;
使用JDK7新加入的MethodHandle;
以下是测试结果:
REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.
Benchmark Mode Cnt Score Error Units
ReflectionBenchmark1.testMethodHandle avgt 25 8.293 ± 0.162 ns/op
ReflectionBenchmark1.testNormal avgt 25 5.761 ± 0.183 ns/op
ReflectionBenchmark1.testReflectedInstance avgt 25 6.257 ± 0.281 ns/op
ReflectionBenchmark1.testReflectedMethod avgt 25 10.838 ± 0.669 ns/op
ReflectionBenchmark1.testReflectedMethodAndInstance avgt 25 11.214 ± 0.623 ns/op
为了保证测试结果的可靠性,我们更换调用方法的形式:
package benchmark;
import org.openjdk.jmh.annotations.*;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
@State(Scope.Thread)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@BenchmarkMode(Mode.AverageTime)
public class ReflectionBenchmark2 {
private static final int len = 1000;
private static final Method applyMethod;
private static final MethodHandle methodHandle;
static {
try {
applyMethod = StringOp.class.getMethod("apply", String.class);
methodHandle =
MethodHandles.lookup()
.findVirtual(
StringOp.class, "apply", MethodType.methodType(String.class, String.class));
} catch (Exception e) {
throw new Error(e);
}
}
private final String[] a;
private final Function instancedObj;
private final Function reflectedObj;
private int index = 0;
{
a = new String[len];
for (int i = 0; i < a.length; i++) a[i] = UUID.randomUUID().toString();
instancedObj = new StringOp();
try {
reflectedObj = (StringOp) Class.forName(getInnerClassName(StringOp.class)).newInstance();
} catch (Exception e) {
throw new Error(e);
}
}
private static String getInnerClassName(Class> clazz) {
String className = clazz.getCanonicalName();
int index = className.lastIndexOf('.');
return className.substring(0, index) + '$' + className.substring(index + 1);
}
@Benchmark
public String testNormal() {
if (index >= len) index = 0;
return instancedObj.apply(a[index++]);
}
@Benchmark
public String testReflectedInstance() {
if (index >= len) index = 0;
return reflectedObj.apply(a[index++]);
}
@Benchmark
public String testReflectedMethod() {
if (index >= len) index = 0;
try {
return (String) applyMethod.invoke(instancedObj, a[index++]);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new IllegalArgumentException(e);
}
}
@Benchmark
public String testReflectedMethodAndInstance() {
if (index >= len) index = 0;
try {
return (String) applyMethod.invoke(reflectedObj, a[index++]);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new IllegalArgumentException(e);
}
}
@Benchmark
public String testMethodHandle(){
if (index >= len) index = 0;
try {
return (String) methodHandle.invoke(instancedObj, a[index++]);
} catch (Throwable e) {
throw new IllegalArgumentException(e);
}
}
public static final class StringOp implements Function {
@Override
public String apply(String s) {
return s.substring(0, 10);
}
}
}
测试结果:
Benchmark Mode Cnt Score Error Units
ReflectionBenchmark2.testMethodHandle avgt 25 13.495 ± 0.276 ns/op
ReflectionBenchmark2.testNormal avgt 25 13.057 ± 0.625 ns/op
ReflectionBenchmark2.testReflectedInstance avgt 25 13.338 ± 1.164 ns/op
ReflectionBenchmark2.testReflectedMethod avgt 25 17.794 ± 1.510 ns/op
ReflectionBenchmark2.testReflectedMethodAndInstance avgt 25 15.668 ± 0.446 ns/op
上面的测试结果说明:对象是直接实例化,还是通过反射动态实例化,对方法的执行性能几乎没有影响;
相比于直接调用对象的方法,通过反射动态调用实例的方法会带来性能下降,性能下降的幅度和方法具体执行的操作有关。
通常情况下MethodHandler的性能虽然比不上直接调用,但也比反射快。
为什么反射会带来性能损失
同样是参考Oracle的官方文档上的话: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.
大意是说:反射的类型是动态解析的,这将导致JVM无法实施某些特定的优化(具体来说,就是我们常说的JIT优化),在性能敏感和频繁调用的方法上,应该尽量避免使用反射。
为什么使用反射在大多数的使用反射的场景中反射带来的性能损失并不是性能的瓶颈(当然不包括反射API的初始化部分):比如spring的web服务器,性能的瓶颈往往在IO和线程调度上;
通过下面一个例子,你将会理解反射不可替代的作用:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.LinkedHashMap;
import java.util.Map;
public class ReflectionDemo {
private static final Map fieldMap;
static {
Map map = new LinkedHashMap<>();
for (Field field : ReflectionDemo.class.getDeclaredFields()) {
if (Modifier.isStatic(field.getModifiers()) || !field.isAnnotationPresent(Alias.class))
continue;
field.setAccessible(true);
try {
Alias alias = field.getAnnotation(Alias.class);
map.put(field, alias.value());
} catch (Exception e) {
throw new Error(e);
}
}
fieldMap = map;
}
@Alias("alias4F1")
private final String f1;
@Alias("alias4F2")
private final String f2;
// more fields...
public ReflectionDemo(String f1, String f2) {
this.f1 = f1;
this.f2 = f2;
}
public String toString1() {
StringBuilder builder = new StringBuilder();
builder.append("alias4F1").append('=').append(f1).append(',');
builder.append("alias4F2").append('=').append(f2).append(',');
// ... return builder.toString();
}
public String toString2() {
StringBuilder builder = new StringBuilder();
fieldMap.forEach(
(k, v) -> {
builder.append(v).append('=');
try {
builder.append(k.get(this));
} catch (IllegalAccessException e) {
throw new IllegalArgumentException(e);
}
builder.append(',');
});
return builder.toString();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
private @interface Alias {
String value();
}
}
上面的两个toString方法,运行的结果都是一样的:
public static void main(String[] args) {
ReflectionDemo demo = new ReflectionDemo("aaa", "bbb");
System.out.println(demo.toString1());
System.out.println(demo.toString2());
}
//alias4F1=aaa,alias4F2=bbb,//alias4F1=aaa,alias4F2=bbb,
toString1()固然有着良好的性能,但是需要通过代码来维护字段和每个字段的别名之间的一个映射,这样做导致的后果是:在字段非常多的时候,这段代码将变得非常的臃肿和繁琐,随着版本的更迭,可能会增删字段,修改别名,也可能有些字段需要打印,有些字段不用打印,这些修改都极容易造成字段的别名的对应出问题产生BUG;toString2()借助反射和注解就从根本上的解决了这个问题,不管字段如何膨胀,类如何修改,都不需要对toString2()方法作出修改。