2024最新Java反射性能优化进阶

以下论点均基于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 的性能确实要优于其他方式,值得一提的是asTypeinvoke也有不小的性能加速,加速后两者在性能上的差距不大,如果不是为了极致性能invoke在使用体验和性能上是一个不错的折中,而invokeWithArguments的性能非常之差甚至要比method的反射还要差的多,笔者暂时还没发现非它不可的场景,这里给出的建议是永远不要使用invokeWithArguments。

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

  • LambdaMetafactory
    LambdaMetafactory是基于方法句柄之上的,它允许你在运行时动态地创建函数式接口的实例,它几乎和直接访问性能接近(https://www.optaplanner.org/blog/2018/01/09/JavaReflectionButMuchFaster.html 中提到其大约慢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是一个综合性成本最低的方案。
原文地址:https://pebble-skateboard-d46.notion.site/Java-7d1e6f877c9d4d02811e1181bc5b361c?pvs=74

参考资料

  1. https://stackoverflow.com/questions/48318779/java-reflection-the-fast-way-to-retrieve-value-from-property
  2. https://www.optaplanner.org/blog/2018/01/09/JavaReflectionButMuchFaster.html
  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. https://docs.oracle.com/javase/8/docs/api/java/lang/invoke/MethodHandle.html
  8. https://docs.oracle.com/javase/8/docs/api/java/lang/invoke/LambdaMetafactory.html#metafactory-java.lang.invoke.MethodHandles.Lookup-java.lang.String-java.lang.invoke.MethodType-java.lang.invoke.MethodType-java.lang.invoke.MethodHandle-java.lang.invoke.MethodType-
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,245评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,749评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,960评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,575评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,668评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,670评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,664评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,422评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,864评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,178评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,340评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,015评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,646评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,265评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,494评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,261评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,206评论 2 352

推荐阅读更多精彩内容