3年前 (2020-11-04)  Java系列 |   抢沙发  2759 
文章评分 0 次,平均分 0.0

替换反射API调用可以提高性能

Java反射替代方案

有时,作为开发人员,您可能会遇到这样的情况:无法使用new运算符实例化对象,因为它的类名存储在配置XML中的某个位置,或者您需要调用名称指定为注释属性的方法。在这种情况下,您总是有一个答案:使用反射

您可以通过存储java.lang.reflect.Method实例,并像在许多框架中实现一样调用它们,但我们决定看看其他选项。反射调用有其成本,如果您开发一个生产类框架,即使是微小的改进也可能在短时间内得到回报。

在本文中,我们将研究反射API及其使用的优缺点,并回顾替代反射API调用的其他选项

根据维基百科上对反射的解释:

反射是计算机程序在运行时检查、和修改其自身结构和行为的能力。

对于大多数Java开发人员来说,反射并不是一件新鲜事,它在许多情况下都被使用。我敢说,没有反射,Java不会变成现在的样子。只需考虑注释处理、数据序列化、通过注释或配置文件绑定方法。对于最流行的IoC框架,反射API是一个基石,因为它广泛地使用了类代理、方法引用等。此外,您还可以将面向方面的编程添加到此列表中。一些AOP框架依赖反射来拦截方法执行。

反射有什么问题吗?我们可以想到其中三种:

  • 速度,反射调用比直接调用慢。我们可以看到每一个JVM版本的反射API性能都有了很大的提高,JIT编译器的优化算法也越来越好,但是反射方法的调用仍然比直接调用慢三倍左右。
  • 类型安全-如果在代码中使用方法引用,则它只是一个方法引用。如果编写的代码通过引用调用方法并传递错误的参数,则调用将在运行时失败,而不是在编译时或加载时。
  • 可跟踪性-如果反射方法调用失败,可能很难找到导致此问题的代码行,因为堆栈跟踪通常很大。您需要深入研究所有这些invoke()proxy()调用。

但如果您研究一下Spring中的事件侦听器实现或Hibernate中的JPA回调,您会发现java.lang.reflect.Method内部方法引用。我怀疑它在不久的将来会改变;成熟的框架是大而复杂的,在许多关键任务系统中都有使用,因此开发人员应该谨慎地引入大的变化。

让我们看看其他的选择。

AOT编译和代码生成:再次提高应用程序的速度

反射替换的第一个候选对象是代码生成。现在,我们可以看到像 Micronaut 和 Quarkus 这样的新框架的兴起,它们的目标有两个:

  1. 快速启动时间
  2. 低内存占用

这两个指标在微服务和无服务器应用程序的时代至关重要。最近的框架正试图通过使用提前编译和代码生成来彻底消除反射。通过使用注释处理、类型访问者和其他技术,它们将直接方法调用、对象实例化等添加到代码中,从而使应用程序更快。它们在启动期间不使用类Class.newInstance(),不要在侦听器中使用反射方法调用等。它看起来很有前途,但是这里有什么折衷吗?答案是:是的。

首先,您可以运行不完全属于您的代码。代码生成会更改原始代码;因此,如果出现错误,则无法判断是您的错误还是代码处理算法中的故障。别忘了现在你应该调试生成的代码,而不是你的代码。

第二个权衡是必须使用供应商提供的单独工具/插件才能使用框架。你不能“仅仅”运行代码,你应该以一种特殊的方式预处理它。如果您在生产中使用框架,您应该将供应商的错误修复应用于框架代码库和代码处理工具。

代码生成已经有很长一段时间了;它没有出现在 MicronautQuarkus 中。例如,在CUBA中,我们使用定制Grails插件和Javassist库在编译时使用类增强。我们添加了一个额外的代码来生成实体更新事件,并将bean验证消息作为字符串字段包含到类代码中,以实现良好的UI表示。

但是实现事件监听器的代码生成看起来有点极端,因为它需要彻底改变内部体系结构。有没有一种东西叫反射但速度更快?

lambdametFactory:更快的方法调用

在Java7中,我们介绍了一个新的JVM指令——InvokedDynamic。最初针对基于JVM的动态语言实现,它已经成为API调用的一个很好的替代品。这个API可以使我们比传统反射提高性能。还有一些特殊类可以在Java代码中构造InvokedDynamic调用:

  • MethodHandle—这个类是在Java7中引入的,但它仍然不太出名。
  • LambDametFactory是在Java8中引入的。这是动态调用思想的进一步发展。此API基于MethodHandle

MethodHandle API 是标准反射的一个很好的替代品,因为JVM在创建MethodHandle时只执行一次所有调用前检查。方法句柄是对底层方法、构造函数、字段或类似低级操作的类型化、直接可执行引用,具有参数或返回值的可选转换。

令人惊讶的是,与反射API相比,纯MethodHandle引用调用没有提供更好的性能,除非您将MethodHandle引用静态化,如此电子邮件列表中所讨论的那样。

但是lambdametfactory是另一回事——它允许我们在运行时生成一个函数接口的实例,该实例包含对MethodHandle解析的方法的引用。使用这个lambda对象,我们可以直接调用被引用的方法。下面是一个例子:

private BiConsumer createVoidHandlerLambda(Object bean, Method method) throws Throwable {
​
  MethodHandles.Lookup caller = MethodHandles.lookup();
  CallSite site = LambdaMetafactory.metafactory(caller,
          "accept",
          MethodType.methodType(BiConsumer.class),
          MethodType.methodType(void.class, Object.class, Object.class),
          caller.findVirtual(bean.getClass(), method.getName(),
                  MethodType.methodType(void.class, method.getParameterTypes()[0])),
          MethodType.methodType(void.class, bean.getClass(), method.getParameterTypes()[0]));
  MethodHandle factory = site.getTarget();
  BiConsumer listenerMethod = (BiConsumer) factory.invoke();
  return listenerMethod;
}

请注意,使用这种方法,我们可以使用java.util.function.BiConsumer代替java.lang.reflect.Method。因此,它不需要太多的重构。让我们考虑一个事件侦听器处理程序代码;它是 Spring 框架的简化改编:

public class ApplicationListenerMethodAdapter implements GenericApplicationListener {

  private final Method method;

  public void onApplicationEvent(ApplicationEvent event) {

      Object bean = getTargetBean();

      Object result = this.method.invoke(bean, event);

      handleResult(result);

  }
}

这就是如何使用基于Lambda的方法引用来更改它:


public class ApplicationListenerLambdaAdapter extends ApplicationListenerMethodAdapter {

  private final BiFunction funHandler;

  public void onApplicationEvent(ApplicationEvent event) {

      Object bean = getTargetBean();

      Object result = handler.apply(bean, event);

      handleResult(result);

  }

}

代码有细微的变化,但功能是相同的。此外,与传统反射相比,它还有一些优势:

  • 类型安全-你可以指定方法签名LambdaMetafactory.metafactory因此,不能将“just”方法绑定为事件侦听器。
  • 可跟踪性-lambda包装器只向方法调用堆栈跟踪添加一个额外的调用。它使调试更加容易。
  • 速度-这是一个需要衡量的东西。

基准测试 Benchmarking

对于新版本的CUBA框架,我们创建了一个基于JMHmicrobenchmark来比较“传统”反射方法调用(基于lambda的方法)的执行时间和吞吐量,并且添加了直接方法调用来进行比较。方法引用和lambda都是在测试执行之前创建和缓存的。

我们使用了以下基准测试参数:

@BenchmarkMode({Mode.Throughput, Mode.AverageTime})
@Warmup(iterations = 5, time = 1000, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 10, time = 1000, timeUnit = TimeUnit.MILLISECONDS)

对于 JVM 11.0.2 和 JMH 1.21,我们得到了以下结果(每个运行的数字可能略有不同):

Java反射替代方案

如您所见,基于lambda的方法处理程序平均快30%。这里讨论的是基于lambda的方法调用性能。结果是lambdametfactory生成的类可以内联,从而获得一些性能改进。而且它比反射更快,因为反射调用必须在每次调用时通过安全检查。

这个基准测试相当贫乏,没有考虑类层次结构、final方法等,它只测量“just”方法调用。然而,就我们的目的而言,这已经足够了。

结论

尽管最近引入了新一代的框架(MicronautQuarkus),与“传统”框架相比有一些优势,但由于Spring,有大量基于反射的代码。我们将看到市场在不久的将来会发生怎样的变化,但是现在,Spring 显然是Java应用程序框架中的领头羊。因此,我们将在相当长的时间内处理反射API。

如果您考虑在代码中使用反射API,无论您是实现自己的框架还是只是一个应用程序,请考虑其他两个选项:代码生成,尤其是LambdaMetafactory。后者将提高代码执行速度,而与“传统”反射API使用相比,开发不会花费更多时间。

 

除特别注明外,本站所有文章均为老K的Java博客原创,转载请注明出处来自https://javakk.com/762.html

关于

发表评论

表情 格式

暂无评论

登录

忘记密码 ?

切换登录

注册