反射慢?慢多少你测过吗#
“反射很慢,尽量别用。“这话你肯定听过。但到底慢多少?慢在哪?大部分人说不清楚,我之前也说不清楚,干脆自己测了一下。
怎么获取 Class 对象#
反射的入口是 Class 对象,三种方式:
// 方式1:编译期就确定了
Class<?> clazz = String.class;
// 方式2:从已有对象获取
Class<?> clazz = "hello".getClass();
// 方式3:运行时按类名加载
Class<?> clazz = Class.forName("java.lang.String");方式 1 和 2 编译期就确定类型了,方式 3 运行时按名字找,最灵活也最慢。
拿到 Class 之后就能获取方法、字段、构造器:
Method method = clazz.getDeclaredMethod("substring", int.class);
Field[] fields = clazz.getDeclaredFields();
Object obj = clazz.getDeclaredConstructor().newInstance();getDeclaredXxx 拿本类声明的(包括 private),getXxx 拿 public 的(包括继承的)。这个区别我之前搞混过,拿不到 private 字段 debug 了半天。
Method.invoke 干了啥#
Method method = clazz.getDeclaredMethod("length");
Object result = method.invoke("hello"); // 返回 5private 方法要先 method.setAccessible(true) 开权限。
invoke 底层干了几件事:
- 检查访问权限
- 检查参数类型匹配
- 调用次数不到 15 次时走 JNI(native 实现)
- 超过 15 次后 JVM 动态生成字节码类来做调用
前 15 次用 native 是因为生成字节码本身有成本,调用少的话不划算。阈值可以通过 -Dsun.reflect.inflationThreshold 调。
实测到底慢多少#
写了段简单的 benchmark:
public class ReflectionBenchmark {
public static void main(String[] args) throws Exception {
MyService service = new MyService();
Method method = MyService.class.getDeclaredMethod("doSomething");
method.setAccessible(true);
// 预热,让 JIT 充分优化
for (int i = 0; i < 100000; i++) {
service.doSomething();
method.invoke(service);
}
int times = 10_000_000;
long start = System.nanoTime();
for (int i = 0; i < times; i++) {
service.doSomething();
}
long directTime = System.nanoTime() - start;
start = System.nanoTime();
for (int i = 0; i < times; i++) {
method.invoke(service);
}
long reflectTime = System.nanoTime() - start;
System.out.println("直接调用: " + directTime / 1_000_000 + "ms");
System.out.println("反射调用: " + reflectTime / 1_000_000 + "ms");
}
}我电脑上(JDK 11)跑了几次,反射大概比直接调用慢 3-5 倍。这是已经过了 15 次阈值、用上动态字节码之后的数据。
3-5 倍听着吓人,但看绝对值——一千万次反射调用也就几十毫秒。你的业务逻辑、数据库查询花的时间比这大好几个数量级。
为什么反射慢#
几个原因:
没法内联。 JIT 对直接调用可以做方法内联,把目标方法代码直接嵌到调用处。反射的目标方法不确定,JIT 做不了这个优化。
装箱拆箱。 invoke 参数是 Object 数组,基本类型要装箱,返回值也是 Object,可能还要拆箱。
权限检查。 每次 invoke 都检查访问权限。setAccessible(true) 之后能省掉这个开销。
方法查找。 getDeclaredMethod 每次都在方法列表里搜索。所以 Method 对象一定要缓存复用,别每次调用都重新获取。
Spring 里的反射#
你可能觉得反射离业务代码很远,其实 Spring 到处在用。
依赖注入:@Autowired 底层就是拿到 Field,setAccessible(true),然后 field.set(bean, value)。
AOP:JDK 动态代理的 InvocationHandler.invoke 最终通过 Method.invoke 调用目标方法。
Bean 创建:Spring 容器用反射调构造器创建 Bean 实例。
注解扫描:@Controller、@Service 这些注解,启动时通过反射读取类上的注解信息。
Spring 整个框架建立在反射之上。启动时用反射做初始化,运行时热路径尽量避开反射,这是一种合理的权衡。
话说回来,如果你对性能有极致要求(比如写序列化框架),可以看看 MethodHandle 或者直接用字节码生成(ASM、Javassist 之类的)。但大部分业务代码,反射性能完全够用,别过早优化。