【Java踩坑笔记】27_大量使用反射,性能到底差多少?
27 | 大量使用反射性能到底差多少摘要反射调用比直接调用慢 10~100 倍取决于 JVM 是否做了优化。如果高频调用必须缓存Method对象否则性能会很差。一、问题现象publicclassReflectionPerfTest{publicstaticvoidmain(String[]args)throwsException{UserusernewUser(Alice);// 方式一直接调用longstartSystem.nanoTime();for(inti0;i100_0000;i){user.getName();}System.out.println(直接调用(System.nanoTime()-start)/1_000_000ms);// 方式二反射调用不缓存 MethodMethodmethodUser.class.getMethod(getName);startSystem.nanoTime();for(inti0;i100_0000;i){method.invoke(user);}System.out.println(反射调用缓存Method(System.nanoTime()-start)/1_000_000ms);// 方式三反射调用每次都 getMethodstartSystem.nanoTime();for(inti0;i100_0000;i){User.class.getMethod(getName).invoke(user);// ❌ 每次都 getMethod}System.out.println(反射调用不缓存(System.nanoTime()-start)/1_000_000ms);}}运行结果参考值JDK 17直接调用约 5ms 反射调用缓存Method约 50ms慢 10 倍 反射调用不缓存约 2000ms慢 400 倍二、踩坑现场场景 1Spring 的反射调用// ❌ 错误每次都反射获取 Methodfor(Orderorder:orders){Methodmorder.getClass().getMethod(getStatus);Objectstatusm.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.inflationThresholdN调整默认 15但每次都getMethod()的查找开销依然无法避免。3.3 缓存 Method 的效果// ❌ 不缓存每次都查找for(...){Methodmclazz.getMethod(getName);// 每次 O(n) 查找m.invoke(obj);}// ✅ 缓存只查找一次Methodmclazz.getMethod(getName);// 只查找一次for(...){m.invoke(obj);// 直接调用JVM 会优化}性能差距缓存后接近直接调用的 1~2 倍JDK 17。四、正确写法4.1 缓存 Method / Field 对象// ✅ 正确缓存 MethodpublicclassBeanUtils{privatestaticfinalMapClass?,Method[]METHOD_CACHEnewConcurrentHashMap();publicstaticMethod[]getMethods(Class?clazz){returnMETHOD_CACHE.computeIfAbsent(clazz,Class::getMethods);// ✅ 缓存}}4.2 用 MethodHandleJava 7性能更好// ✅ MethodHandle 比 Method.invoke() 更快更接近直接调用publicclassMethodHandleTest{publicstaticvoidmain(String[]args)throwsThrowable{UserusernewUser(Alice);MethodHandles.LookuplookupMethodHandles.lookup();MethodTypemtMethodType.methodType(String.class);MethodHandlemhlookup.findVirtual(User.class,getName,mt);// 调用Stringname(String)mh.invoke(user);// ✅ 性能接近直接调用}}4.3 用 LambdaMetafactoryJava 8最快// ✅ 用 LambdaMetafactory 生成函数式接口性能几乎等同于直接调用publicclassLambdaReflection{publicstaticvoidmain(String[]args)throwsException{MethodgetNameUser.class.getMethod(getName);// 将 Method 转成 Function只做一次后续调用无反射开销FunctionUser,StringgetterLambdaMetafactoryUtil.createGetter(getName);UserusernewUser(Alice);Stringnamegetter.apply(user);// ✅ 无反射开销}}4.4 非必要不使用反射// ❌ 可以用接口就不要用反射publicvoidprocess(Objectobj){Methodmobj.getClass().getMethod(process);m.invoke(obj);}// ✅ 用接口publicinterfaceProcessable{voidprocess();}publicvoidprocess(Processableobj){obj.process();// ✅ 直接调用编译期确定}五、最佳实践✅ 反射使用的 4 条规范必须缓存Method/Field对象不要每次都getMethod()/getField()高频调用场景避免使用反射用接口 多态代替框架开发才用反射业务代码尽量不用JDK 17 的反射性能已大幅改善但依然比直接调用慢 反射性能对比JMH 基准测试参考调用方式相对性能倍数直接调用1x基准反射缓存 MethodJDK 171.5x ~ 3x反射缓存 MethodJDK 85x ~ 10x反射不缓存 Method100x ~ 400x️ 阿里巴巴 Java 开发手册规约【强制】避免使用反射调用方法如果避免不了应当对Method对象进行缓存。六、小结反射调用比直接调用慢10~400 倍取决于是否缓存MethodgetMethod()/getField()是主要性能瓶颈必须缓存JDK 17 对反射做了优化缓存后性能接近直接调用1.5x~3x高频场景用MethodHandle或LambdaMetafactory代替反射业务代码避免使用反射用接口 多态代替下一篇预告finalize 方法已被标记废弃还在用就快改——finalize()调用时机不确定还会拖慢 GCJava 9 起已标记废弃。