反射调用 method.invoke() 通常比直接调用慢 3–5 倍,jdk 9 后高频场景可达 10 倍以上,主因是 jvm 无法内联、需每次执行权限检查、参数封装、类型转换和异常包装。

反射调用方法比直接调用慢多少
反射调用 Method.invoke() 通常比直接调用慢 3–5 倍,JDK 9 之后在频繁调用场景下差距可能扩大到 10 倍以上。这不是因为“反射本身慢”,而是 JVM 无法对反射路径做内联、类型推导和 JIT 优化,每次调用都要走完整的权限检查、参数封装(Object[])、类型转换和异常包装流程。
实操建议:
- 避免在高频循环里用
method.invoke(obj, args);可提前缓存Method实例,但不能消除核心开销 - 若必须高频反射调用,考虑用
MethodHandle(JDK 7+)或VarHandle(JDK 9+),它们支持更接近直接调用的优化路径 - 测试时用 JMH,不用
System.nanoTime()简单计时——JIT 预热、代码路径污染会影响结果
Spring 和 MyBatis 为什么敢大量用反射
它们不是“大量用反射”,而是把反射集中在启动阶段(startup phase),运行时几乎不触发。比如 Spring 的 BeanFactory 在容器初始化时扫描所有 @Component 类,通过反射读取注解、构造器、字段,然后生成代理类或字节码增强(CGLIB / JDK Proxy),后续调用走的是纯字节码逻辑。
关键点:
立即学习“Java免费学习笔记(深入)”;
- MyBatis 的
MapperProxy仅在首次获取Mapper接口实例时反射解析 SQL 注解或 XML,之后所有方法调用都走 JDK 动态代理的invoke(),不重复反射 - Spring Boot 的条件化配置(
@ConditionalOnClass)依赖Class.forName(),但它只在应用上下文刷新前执行一次,不参与请求链路 - 真正危险的是运行时按字符串动态加载类并调用——如
Class.forName(className).getDeclaredMethod(methodName).invoke(...),这种模式应加缓存或改用服务发现机制
如何安全地绕过 private 字段访问限制
用 Field.setAccessible(true) 能跳过 Java 访问控制,但自 JDK 12 起受强封装策略(Strong Encapsulation)限制,默认禁止反射访问非模块开放的类成员。强行调用会抛出 InaccessibleObjectException。
正确做法:
- 优先使用公开 API:比如想读
ArrayList.size,用list.size()而非反射字段 - 若必须访问(如单元测试 mock 私有状态),启动 JVM 时加参数:
--add-opens java.base/java.lang=ALL-UNNAMED(针对具体模块和包) - 避免在生产代码中调用
setAccessible(true)——它会被 SecurityManager 拦截,且在 GraalVM Native Image 中完全不可用
Class.forName() vs ClassLoader.loadClass() 的区别
两者都会加载类,但 Class.forName(String) 默认会触发类的初始化(执行 static 块),而 ClassLoader.loadClass(String) 只加载不初始化。
Class.forName("com.example.MyConfig"); // static {} 会执行
Thread.currentThread().getContextClassLoader()
.loadClass("com.example.MyConfig"); // static {} 不执行
常见误用场景:
- 用
Class.forName()加载 JDBC 驱动(如"com.mysql.cj.jdbc.Driver")——这是旧版写法,JDK 6+ 已支持 SPI 自动注册,无需手动触发初始化 - 在 OSGi 或模块化环境里,错误选择加载器导致
NoClassDefFoundError:应优先用当前类的this.getClass().getClassLoader(),而非ClassLoader.getSystemClassLoader() - 热部署/插件系统中,多次调用
Class.forName()会导致重复初始化,应配合Class.isAssignableFrom()先判断是否已加载











