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类中字段的多少并不会显著的影响性能。
那么来到第二个问题针对反射方法而言有哪些方式可以优化呢?
- 字节码生成
- 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-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.
参考资料
- Java Reflection: the fast way to retrieve value from property - Stack Overflow
- OptaPlanner - Java Reflection, but much faster
- https://www.quora.com/Is-Java-Reflection-slow-or-expensive
- https://medium.com/free-code-camp/a-faster-alternative-to-java-reflection-db6b1e48c33e
- https://www.reddit.com/r/java/comments/7p8czw/java_reflection_is_twice_as_slow_as_direct_access/
- https://blogs.oracle.com/javamagazine/post/java-reflection-performance
- MethodHandle (Java Platform SE 8 )
- LambdaMetafactory (Java Platform SE 8 )
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!