以下論點均基於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類中字段的多少並不會顯著的影響性能。
那麼來到第二個問題針對反射方法而言有哪些方式可以優化呢?
- 字節碼生成
- MethodHandle(方法句柄)
- 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的性能確實要優於其他方式,值得一提的是asType對invoke也有不小的性能加速,加速後兩者在性能上的差距不大,如果不是為了極致性能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.
參考資料
- https://stackoverflow.com/questions/48318779/java-reflection-the-fast-way-to-retrieve-value-from-property
- https://www.optaplanner.org/blog/2018/01/09/JavaReflectionBut...
- https://www.quora.com/Is-Java-Reflection-slow-or-expensive
- https://medium.com/free-code-camp/a-faster-alternative-to-jav...
- https://www.reddit.com/r/java/comments/7p8czw/java_reflection...
- https://blogs.oracle.com/javamagazine/post/java-reflection-pe...
- https://docs.oracle.com/javase/8/docs/api/java/lang/invoke/Me...
- https://docs.oracle.com/javase/8/docs/api/java/lang/invoke/La...