Java反射性能详解

发布时间:2023年12月26日

以下论点均基于jdk8但大部分并不限于jdk8

openjdk version "1.8.0_382-internal"
OpenJDK Runtime Environment (build 1.8.0_382-internal-b05)
OpenJDK 64-Bit Server VM (build 25.382-b05, mixed mode)

首先让我们从两个问题出发

1.使用field和get set方法访问修改字段值哪个的性能要更好(均已做了缓存)?

2.怎么优化一个反射方法?

以下为一个简单jmh基准测试结果:

获取字段值方式                           Mode  Cnt  Score   Error  Units
直接获取                                avgt   60  2.011 ± 0.074  ns/op
使用field获取                           avgt   60  3.642 ± 0.219  ns/op
使用get method获取                      avgt   60  4.237 ± 0.113  ns/op
修改字段值方式                           Mode  Cnt  Score   Error  Units
直接修改                                avgt   60  2.855 ± 0.026  ns/op
使用field修改                           avgt   60  5.289 ± 0.241  ns/op
使用set method修改                      avgt   60  6.226 ± 0.253  ns/op

ps: 上述method的性能为Inflation后的性能,反射调用method超过InflationThreshold后 会生成sun.reflect.GeneratedMethodAccessor,在类和字段较多时可能会导致metaSpace OOM (当设置了MaxMetaSpaceSize参数时),可以关闭Inflation机制但会带来性能较为明显的下降.

所以第一个问题的结论就是field的性能要更好,且field不会动态生成类对metaSpace的压力更小,因此如果只是为了获取字段值,field方式始终优于get set方法

值得一提的还有不管是field还是method类中字段的多少并不会显著的影响性能。

那么来到第二个问题针对反射方法而言有哪些方式可以优化呢?

  1. 字节码生成
  2. MethodHandle(方法句柄)
  3. LambdaMetafactory
  • 字节码生成

    基于JavaCompiler和asm/javasist/byteBuddy生成字节码将获得和原生代码类似的性能,基本完全一致,当然最终性能如何依旧取决于你生成代码的质量。

    简单示例代码

    public class JavaCompilerBeanPropertyReaderFactory {
    
      public static BeanPropertyReader generate(Class<?> beanClass, String propertyName) {
        // Not 100% according to Java Beans spec, contains a bug for getHTTP() IIRC
        String getterName =
            "get" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);
        String packageName = JavaCompilerBeanPropertyReaderFactory.class.getPackage().getName()
            + ".generated." + beanClass.getPackage().getName();
        String simpleClassName = beanClass.getSimpleName() + "$" + propertyName;
        String fullClassName = packageName + "." + simpleClassName;
        final String source = "package " + packageName + ";\\n"
            + "public class " + simpleClassName + " implements " + BeanPropertyReader.class.getName()
            + " {\\n"
            + "    public Object executeGetter(Object bean) {\\n"
            + "        return ((" + beanClass.getName() + ") bean)." + getterName + "();\\n"
            + "    }\\n"
            + "}";
        StringGeneratedJavaCompilerFacade compilerFacade = new StringGeneratedJavaCompilerFacade(
            JavaCompilerBeanPropertyReaderFactory.class.getClassLoader());
        Class<? extends BeanPropertyReader> compiledClass = compilerFacade.compile(
            fullClassName, source, BeanPropertyReader.class);
        try {
          return compiledClass.newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
          throw new IllegalStateException(
              "The generated class (" + fullClassName + ") failed to instantiate.", e);
        }
      }
    
    }
    

    使用方式

    BeanPropertyReader javaCompilerBeanPropertyReader = JavaCompilerBeanPropertyReaderFactory.generate(xxxx.class, 字段名);
    javaCompilerBeanPropertyReader.executeGetter(对象);
    

    这里有一个有趣的框架https://github.com/EsotericSoftware/reflectasm如果反射的性能对你真的很重要你可以考虑它

  • MethodHandle(方法句柄)

    很有意思的一个事实是jvm官方自己都打算用MethodHandle重写method的实现。

    JEP 416: Reimplement Core Reflection with Method Handles

    这充分说明了MethodHandle性能的优越,当然不当的使用姿势可能会导致MethodHandle的性能反而比method要更差,比如如果要直接使用MethodHandle那么它应当是static final的(原因见https://shipilev.net/jvm/anatomy-quarks/17-trust-nonstatic-final-fields/ 总结下来就是可以被内联)否则它可能会比method.invoke要更慢。

    以下为基于**invokeExact**的简单jmh基准测试结果:

    获取字段值方式/MethodHandle获取方式                      Mode   Cnt  Score   Error  Units
    DirectAccess                                          avgt   15  2.294 ± 0.122  ns/op
    final + findVirtual            + asType + static      avgt   15  2.485 ± 0.199  ns/op
    final + findVirtual                                   avgt   15  4.798 ± 0.045  ns/op
    final + findVirtual            + asType               avgt   15  4.761 ± 0.050  ns/op
    final + unreflectGetter(field) + asType + static      avgt   15  2.469 ± 0.036  ns/op
    final + unreflectGetter(field)                        avgt   15  5.455 ± 0.668  ns/op
    final + unreflectGetter(field) + asType               avgt   15  5.075 ± 0.103  ns/op
    
    **ps**:
    1.对于一个实例方法来说通过findVirtual和unreflect(method) 获取的MethodHandle是等价的; 
    2.asType会生成一个适配器方法句柄,它将当前方法句柄的类型调整为新类型。保证生成的方法句柄报告的类型等于所需的新类型;
    3.MethodHandles.lookup()不要静态化,MethodHandles.lookup()执行时会获取方法权限,对于private的方法如果使用静态化lookup将获取不到其权限。
    

    从基准测试的结果可以看出来static非static的性能差距明显,asType也总会带来性能的提升,使用MethodHandle时应尽量指定asType

    为什么要用**invokeExact**来测试呢?因为它是方法句柄性能最好的选择。

    • MethodHandle使用方式

      方法句柄严格来说有三个使用方法

      • invokeWithArguments:使用该方法调用方法句柄是这三个选项中限制最少的。实际上,除了对参数和返回类型进行强制转换和装箱/拆箱外,它还允许可变参数数组传入作为方法参数集合调用;
      • invoke:当使用该方法时,我们强制执行固定数量的参数(arity),但允许对参数和返回类型进行强制转换和装箱/拆箱;
      • invokeExact:它不提供对提供的类的任何强制转换,并且需要固定数量的参数。

      对于方法句柄的三种调用方式,性能最好的是**invokeExact,因为它是最精确的调用方式,避免了在运行时的类型转换。invokeExact** 的优势在于,它对参数类型和数量的匹配要求非常严格,这使得在调用时无需进行额外的类型检查和转换。这样可以减少在方法调用时的开销,提高执行效率。然而,需要注意的是,使用**invokeExact要求调用方确保参数类型和数量的准确匹配,否则会在运行时抛出WrongMethodTypeException。因此,在使用时需要确保方法句柄和调用方的代码之间的匹配性。总的来说,性能最好的选择是invokeExact**,但在某些情况下,根据需求的灵活性和对性能的要求,选择其他的调用方式也是有可能的。

      以下是对于三种调用方式的简单jmh基准测试结果:

      获取字段值方式/MethodHandle获取方式            Mode  Cnt    Score    Error  Units
      DirectAccess                               avgt   15    2.269 ±  0.032  ns/op
      invokeExact + asType                       avgt   15    2.466 ±  0.021  ns/op
      invoke + asType                            avgt   15    2.515 ±  0.091  ns/op
      invoke                                     avgt   15    4.805 ±  0.149  ns/op
      invokeWithArguments + asType               avgt   15  119.376 ± 13.407  ns/op
      invokeWithArguments                        avgt   15  117.941 ±  5.479  ns/op
      

      可以看出来明显**invokeExact** 的性能确实要优于其他方式,值得一提的是asType对**invoke也有不小的性能加速,加速后两者在性能上的差距不大,如果不是为了极致性能invoke在使用体验和性能上是一个不错的折中,而invokeWithArguments的性能非常之差甚至要比method的反射还要差的多,笔者暂时还没发现非它不可的场景,这里给出的建议是永远不要使用invokeWithArguments。**

    这里不得不提的还有一个MethodHandleProxies,它对标jdk代理java.lang.reflect.Proxy,由于其基于MethodHandle大部分情况下性能要更好。

  • LambdaMetafactory

    LambdaMetafactory是基于方法句柄之上的,它允许你在运行时动态地创建函数式接口的实例,它几乎和直接访问性能接近(OptaPlanner - Java Reflection, but much faster 中提到其大约慢33%)。

    简单示例代码

    public class LambdaMetafactoryBeanPropertyReader implements BeanPropertyReader {
    
      private final Function getterFunction;
    
      public LambdaMetafactoryBeanPropertyReader(Class<?> beanClass, String propertyName) {
        getterFunction = getFunction(beanClass, propertyName);
      }
    
      public static Function<?, ?> getFunction(Class<?> beanClass, String propertyName) {
        final Function<?, ?> getterFunction;
        // Not 100% according to Java Beans spec, contains a bug for getHTTP() IIRC
        String getterName =
            "get" + propertyName.substring(0, 1).toUpperCase() + propertyName.substring(1);
        Method getterMethod;
        try {
          getterMethod = beanClass.getMethod(getterName);
        } catch (NoSuchMethodException e) {
          throw new IllegalArgumentException(
              "The class (" + beanClass + ") has doesn't have the getter method ("
                  + getterName + ").", e);
        }
        Class<?> returnType = getterMethod.getReturnType();
    
        MethodHandles.Lookup lookup = MethodHandles.lookup();
        CallSite site;
        try {
          site = LambdaMetafactory.metafactory(lookup,
              "apply",
              MethodType.methodType(Function.class),
              MethodType.methodType(Object.class, Object.class),
              lookup.findVirtual(beanClass, getterName, MethodType.methodType(returnType)),
              MethodType.methodType(returnType, beanClass));
        } catch (LambdaConversionException | NoSuchMethodException | IllegalAccessException e) {
          throw new IllegalArgumentException(
              "Lambda creation failed for method (" + getterMethod + ").", e);
        }
        try {
          getterFunction = (Function<?, ?>) site.getTarget().invokeExact();
        } catch (Throwable e) {
          throw new IllegalArgumentException(
              "Lambda creation failed for method (" + getterMethod + ").", e);
        }
        return getterFunction;
      }
    
      public Object executeGetter(Object bean) {
        return getterFunction.apply(bean);
      }
    
    }
    

    使用方式

    LambdaMetafactoryBeanPropertyReader lambdaMetafactoryBeanPropertyReader = new LambdaMetafactoryBeanPropertyReader(xxxx.class, 字段名);
    lambdaMetafactoryBeanPropertyReader.executeGetter(对象);
    
    // 更极致的方式
    // 定义一个function常量
    private static final Function<?, ?> function = LambdaMetafactoryBeanPropertyReader.getFunction(xxxx.class, 字段名);
    // 使用function
    function.apply(对象);
    

注意点

  • 上述MethodHandle的性能为独立适配器的性能且已做缓存MethodHandle一开始持有的适配器是共享的,会在调用超过Djava.lang.invoke.MethodHandle.CUSTOMIZE_THRESHOLD,默认值为127后生成一个**LambdaForm,**之后都是独立的适配器, 也要小心metaSpace的OOM;
  • 反射调用和native方法(除了intrinsic函数)很难被内联;
  • MethodHandle.invoke()虽然是native方法但依旧可以被JIT内联优化
  • 在非科学测量中,使用LambdaMetafactory的元空间成本似乎约为每个 lambda 2kb,并且它会正常进行垃圾回收。
  • LambdaMetaFactory在jdk8中对私有方法有较强的校验,需要使用较为hack的方式才能生成正确的函数
Field internal = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
internal.setAccessible(true);
MethodHandles.Lookup) TRUSTED = (MethodHandles.Lookup) internal.get(null);
MethodHandles.Lookup lookup = TRUSTED.in(xxxx.class);

至于私有字段是搞不定的(因为这个lambda是在另一个类/包中生成的,不能访问那些私有成员)。

下面是获取字段值方式的简单jmh基准测试结果:

获取字段值方式                Mode  Cnt  Score   Error  Units
直接访问                     avgt   60  2.306 ± 0.033  ns/op
方法反射                     avgt   60  4.562 ± 0.077  ns/op
final方法句柄                avgt   60  4.672 ± 0.024  ns/op
static final方法句柄         avgt   60  2.467 ± 0.180  ns/op
字节生成                     avgt   60  2.467 ± 0.180  ns/op
LambdaMetafactory           avgt   60  2.528 ± 0.056  ns/op

以性能而言,static final MethodHandle、字节码生成、LambdaMetafactory 这三种方式都能达到接近直接访问的程度,不管使用何种方式static都有助于JVM进行优化分析,如果要追求极致性能尽量设为static final,但字节码生成得实现和维护成本都过于高昂,在字段不多的情况下选择static化的MethodHandle是一个综合性成本最低的方案。

原文地址:Notion – The all-in-one workspace for your notes, tasks, wikis, and databases.

参考资料

  1. Java Reflection: the fast way to retrieve value from property - Stack Overflow
  2. OptaPlanner - Java Reflection, but much faster
  3. https://www.quora.com/Is-Java-Reflection-slow-or-expensive
  4. https://medium.com/free-code-camp/a-faster-alternative-to-java-reflection-db6b1e48c33e
  5. https://www.reddit.com/r/java/comments/7p8czw/java_reflection_is_twice_as_slow_as_direct_access/
  6. https://blogs.oracle.com/javamagazine/post/java-reflection-performance
  7. MethodHandle (Java Platform SE 8 )
  8. LambdaMetafactory (Java Platform SE 8 )

文章来源:https://blog.csdn.net/Ares_xb/article/details/135169779
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。