以下论点均基于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 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 OverflowOptaPlanner - Java Reflection, but much fasterhttps://www.quora.com/Is-Java-Reflection-slow-or-expensivehttps://medium.com/free-code-camp/a-faster-alternative-to-java-reflection-db6b1e48c33ehttps://www.reddit.com/r/java/comments/7p8czw/java_reflection_is_twice_as_slow_as_direct_access/https://blogs.oracle.com/javamagazine/post/java-reflection-performanceMethodHandle (Java Platform SE 8 )LambdaMetafactory (Java Platform SE 8 )

推荐链接

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。