Stories

Detail Return Return

2025最新Java反射性能優化進階 - Stories Detail

以下論點均基於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(默認15)
會生成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-fi... 總結下來就是可以被內聯)否則它可能會比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/JavaReflectionBut... 中提到其大約慢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. https://stackoverflow.com/questions/48318779/java-reflection-the-fast-way-to-retrieve-value-from-property
  2. https://www.optaplanner.org/blog/2018/01/09/JavaReflectionBut...
  3. https://www.quora.com/Is-Java-Reflection-slow-or-expensive
  4. https://medium.com/free-code-camp/a-faster-alternative-to-jav...
  5. https://www.reddit.com/r/java/comments/7p8czw/java_reflection...
  6. https://blogs.oracle.com/javamagazine/post/java-reflection-pe...
  7. https://docs.oracle.com/javase/8/docs/api/java/lang/invoke/Me...
  8. https://docs.oracle.com/javase/8/docs/api/java/lang/invoke/La...
user avatar mannayang Avatar king_wenzhinan Avatar u_16769727 Avatar lenve Avatar mulavar Avatar daimajiangxin Avatar beishangdeyadan Avatar jeecg Avatar wuxiedekeben Avatar litongjava Avatar chaokunyang Avatar knifeblade Avatar
Favorites 13 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.