27 | 大量使用反射,性能到底差多少?
摘要:反射调用比直接调用慢 10~100 倍(取决于 JVM 是否做了优化)。如果高频调用,必须缓存
Method对象,否则性能会很差。
一、问题现象
publicclassReflectionPerfTest{publicstaticvoidmain(String[]args)throwsException{Useruser=newUser("Alice");// 方式一:直接调用longstart=System.nanoTime();for(inti=0;i<100_0000;i++){user.getName();}System.out.println("直接调用:"+(System.nanoTime()-start)/1_000_000+"ms");// 方式二:反射调用(不缓存 Method)Methodmethod=User.class.getMethod("getName");start=System.nanoTime();for(inti=0;i<100_0000;i++){method.invoke(user);}System.out.println("反射调用(缓存Method):"+(System.nanoTime()-start)/1_000_000+"ms");// 方式三:反射调用(每次都 getMethod)start=System.nanoTime();for(inti=0;i<100_0000;i++){User.class.getMethod("getName").invoke(user);// ❌ 每次都 getMethod}System.out.println("反射调用(不缓存):"+(System.nanoTime()-start)/1_000_000+"ms");}}运行结果(参考值,JDK 17):
直接调用:约 5ms 反射调用(缓存Method):约 50ms(慢 10 倍) 反射调用(不缓存):约 2000ms(慢 400 倍)二、踩坑现场
场景 1:Spring 的反射调用
// ❌ 错误:每次都反射获取 Methodfor(Orderorder:orders){Methodm=order.getClass().getMethod("getStatus");Objectstatus=m.invoke(order);// 每次都 getMethod,性能极差}场景 2:序列化框架里大量反射
很多 JSON 框架(如 FastJSON、Jackson)第一次序列化某个类时会用反射,但会缓存Field/Method,后续调用不会再反射查找。
但如果缓存失效(如用WeakReference缓存,被 GC 了),就会重新反射查找,导致性能抖动。
三、原理解析
3.1 反射调用为什么慢?
直接调用:编译期就确定了方法地址,调用时直接跳过去(CPU 分支预测友好)。
反射调用(Method.invoke()):
getMethod()需要遍历类的方法列表(O(n) 查找)invoke()内部需要检查访问权限(可访问性检查)invoke()需要用Varables包装参数(装箱开销)invoke()无法被内联优化(JVM 动态调用,JIT 难以优化)
3.2 JVM 对反射的优化
JDK 1.8+ 对反射做了优化:
Method.invoke()在调用一定次数后,会生成字节码代理(不再用 native 方法,性能接近直接调用)- 这个阈值可以通过
-Dsun.reflect.inflationThreshold=N调整(默认 15)
但:每次都getMethod()的查找开销依然无法避免。
3.3 缓存 Method 的效果
// ❌ 不缓存:每次都查找for(...){Methodm=clazz.getMethod("getName");// 每次 O(n) 查找m.invoke(obj);}// ✅ 缓存:只查找一次Methodm=clazz.getMethod("getName");// 只查找一次for(...){m.invoke(obj);// 直接调用,JVM 会优化}性能差距:缓存后接近直接调用的 1~2 倍(JDK 17+)。
四、正确写法
4.1 缓存 Method / Field 对象
// ✅ 正确:缓存 MethodpublicclassBeanUtils{privatestaticfinalMap<Class<?>,Method[]>METHOD_CACHE=newConcurrentHashMap<>();publicstaticMethod[]getMethods(Class<?>clazz){returnMETHOD_CACHE.computeIfAbsent(clazz,Class::getMethods);// ✅ 缓存}}4.2 用 MethodHandle(Java 7+,性能更好)
// ✅ MethodHandle 比 Method.invoke() 更快(更接近直接调用)publicclassMethodHandleTest{publicstaticvoidmain(String[]args)throwsThrowable{Useruser=newUser("Alice");MethodHandles.Lookuplookup=MethodHandles.lookup();MethodTypemt=MethodType.methodType(String.class);MethodHandlemh=lookup.findVirtual(User.class,"getName",mt);// 调用Stringname=(String)mh.invoke(user);// ✅ 性能接近直接调用}}4.3 用 LambdaMetafactory(Java 8+,最快)
// ✅ 用 LambdaMetafactory 生成函数式接口,性能几乎等同于直接调用publicclassLambdaReflection{publicstaticvoidmain(String[]args)throwsException{MethodgetName=User.class.getMethod("getName");// 将 Method 转成 Function(只做一次,后续调用无反射开销)Function<User,String>getter=LambdaMetafactoryUtil.createGetter(getName);Useruser=newUser("Alice");Stringname=getter.apply(user);// ✅ 无反射开销}}4.4 非必要不使用反射
// ❌ 可以用接口就不要用反射publicvoidprocess(Objectobj){Methodm=obj.getClass().getMethod("process");m.invoke(obj);}// ✅ 用接口publicinterfaceProcessable{voidprocess();}publicvoidprocess(Processableobj){obj.process();// ✅ 直接调用,编译期确定}五、最佳实践
✅ 反射使用的 4 条规范
- 必须缓存
Method/Field对象,不要每次都getMethod()/getField() - 高频调用场景避免使用反射,用接口 + 多态代替
- 框架开发才用反射,业务代码尽量不用
- JDK 17+ 的反射性能已大幅改善,但依然比直接调用慢
🔍 反射性能对比(JMH 基准测试参考)
| 调用方式 | 相对性能(倍数) |
|---|---|
| 直接调用 | 1x(基准) |
| 反射(缓存 Method,JDK 17+) | 1.5x ~ 3x |
| 反射(缓存 Method,JDK 8) | 5x ~ 10x |
| 反射(不缓存 Method) | 100x ~ 400x |
🛠️ 阿里巴巴 Java 开发手册规约
【强制】避免使用反射调用方法,如果避免不了,应当对
Method对象进行缓存。
六、小结
- 反射调用比直接调用慢10~400 倍(取决于是否缓存
Method) getMethod()/getField()是主要性能瓶颈,必须缓存- JDK 17+ 对反射做了优化,缓存后性能接近直接调用(1.5x~3x)
- 高频场景用
MethodHandle或LambdaMetafactory代替反射 - 业务代码避免使用反射,用接口 + 多态代替
下一篇预告:finalize 方法已被标记废弃,还在用就快改——finalize()调用时机不确定,还会拖慢 GC,Java 9 起已标记废弃。