以下论点均基于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类中字段的多少并不会显著的影响性能。
那么来到第二个问题针对反射方法而言有哪些方式可以优化呢?
字节码生成
基于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(对象);
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.