反射太慢了?那是你不会用LambdaMetafactory!
引言
在 Java 的世界里,反射一直是开发者们既爱又恨的功能
一方面,它提供了极大的灵活性,另一方面,反射带来的性能开销却让人头疼不已
在之前的文章中,我们介绍使用 Spring 工具类 ReflectionUtils 通过缓存、避免创建临时对象的方式来优化反射的性能
但工具类在调用方法时依旧会使用反射的 invoke,在高频调用的场景性能还是会与直接调用相差一大截
随着Java8的到来,LambdaMetafactory 为我们打开了新的大门,它「不仅能够使用类似反射的灵活性,在高频调用场景下还能与直接调用的性能相差不大」
本文将深入浅出的介绍 LambdaMetafactory,从使用、性能测试、原理、应用等方面阐述它所带来的优势
使用 LambdaMetafactory
LambdaMetafactory 是 Java 8 引入的一个类,位于java.lang.invoke包下
它的主要任务是在运行时创建 lambda 表达式的实现
LambdaMetafactory 通过 MethodHandle 和 CallSite 机制工作,它能够「在运行时通过 ASM 字节码库生成 lambda 内部类来调用目标方法,从而兼得反射的灵活性与直接调用的性能优势」
MethodHandle 是方法句柄,通过它可以灵活调用目标方法;CallSite 用于动态管理存储方法句柄 MethodHandle,它们是实现动态调用的关键
LambdaMetafactory 的核心在于其LambdaMetafactory.metafactory方法
该方法根据目标方法、接口方法等数据来定义目标 lambda 的行为,并返回一个 CallSite 对象
后续通过获取 CallSite 的目标方法句柄进行调用
使用方法通常分为以下几个步骤:
使用 Lookup 查找要调用的目标方法句柄 MethodHandle 根据要生成的 lambda 方法名、调用接口、目标方法类型(返回、入参类型)、句柄等信息创建 CallSite 根据 CallSite 的 MethodHandle 获取生成的函数接口实现类再调用接口方法
❝定义的函数式接口
❞
@FunctionalInterfaceinterface Greeter {void greet(String name);}
❝使用 LambdaMetafactory
❞
public class LambdaMetafactoryExample {public static void sayHello(String name) {System.out.println("Hello, " + name);}public static void main(String[] args) throws Throwable {// 获取 Lookup 对象MethodHandles.Lookup lookup = MethodHandles.lookup();// 编写方法类型 返回、入参类型MethodType methodType = MethodType.methodType(void.class, String.class);// 查找目标方法句柄MethodHandle targetMethod = lookup.findStatic(LambdaMetafactoryExample.class, "sayHello", methodType);// 准备元数据并创建 CallSiteCallSite callSite = LambdaMetafactory.metafactory(lookup,"greet", // 要生成的lambda方法名MethodType.methodType(Greeter.class), // 调用点签名methodType, // 目标方法类型(注意这里应该直接是 methodType)targetMethod, // 目标方法句柄methodType // 目标方法类型);// 获取并调用MethodHandle factory = callSite.getTarget();Greeter greeter = (Greeter) factory.invokeWithArguments();// 调用接口方法greeter.greet("World");}}
总的来说,「LambdaMetafactory 就是通过 lambda 生成接口方法,从而实现灵活调用目标方法」
性能对比
反射性能上带来的劣势主要有以下几个原因:
「调用时需要安全检查、类型转换,查找到方法、字段后会创建临时对象」 「方法、字段的缓存不是强引用,gc 后会被清空」 「动态运行,高频访问无法使用 JIT 优化」
JVM 通过解释、编译混合运行,对于高频访问的代码会使用 JIT 将字节码转化为机器码缓存在方法区,无需再进行解释执行,在高频访问的场景下反射无法使用该优化
而 LambdaMetafactory 相比反射具有显著的性能优势,主要原因在于:
「减少开销:减少每次调用时的安全检查和类型转换等操作,使用 Lookup 进行查找」 「避免重复计算:一旦 lambda 被初始化,后续调用几乎无额外开销」 「即时编译:生成的代码可以被 JVM 即时编译器 (JIT) 优化」
这些因素使得 LambdaMetafactory 在循环、频繁调用的场景中尤为出色
测试代码如下,也可以直接看后面的结果表格:
public class LambdaVsReflectionBenchmark {// 目标方法publicstaticvoidsayHello(String name) {name = "Hello," + name;// System.out.println("Hello, " + name);}// 使用 LambdaMetafactory 创建 lambda 表达式privatestatic Greeter createLambda() throws Throwable {MethodHandles.Lookup lookup = MethodHandles.lookup();MethodType methodType = MethodType.methodType(void.class, String.class);MethodHandle targetMethod = lookup.findStatic(LambdaVsReflectionBenchmark.class, "sayHello", methodType);CallSite callSite = LambdaMetafactory.metafactory(lookup,"greet",MethodType.methodType(Greeter.class),methodType.changeReturnType(void.class),targetMethod,methodType);MethodHandle factory = callSite.getTarget();return (Greeter) factory.invokeExact();}// 使用反射获取目标方法privatestatic Method getReflectiveMethod() throws Exception {return LambdaVsReflectionBenchmark.class.getMethod("sayHello", String.class);}// 测试调用次数// private static final long ITERATIONS = 1_000L;private static final long ITERATIONS = 1_000_000_000L;/*** 调用次数:1000* 直接调用耗时: 0.13 ms* LambdaMetafactory 调用耗时: 0.17 ms* 反射调用耗时: 1.74 ms* <p>* 调用次数:1000000000* 直接调用耗时: 4501.54 ms* LambdaMetafactory 调用耗时: 4640.59 ms* 反射调用耗时: 6142.39 ms** @param args* @throws Throwable*/publicstaticvoidmain(String[] args) throws Throwable {System.out.println("调用次数:" + ITERATIONS);//直接调用long start = System.nanoTime();for (int i = 0; i < ITERATIONS; i++) {sayHello("World");}long end = System.nanoTime();System.out.printf("直接调用耗时: %.2f ms%n", (end - start) / 1e6);// 准备 LambdaGreeter lambdaGreeter = createLambda();// 测试 Lambda 调用long lambdaStart = System.nanoTime();for (int i = 0; i < ITERATIONS; i++) {lambdaGreeter.greet("World");}long lambdaEnd = System.nanoTime();System.out.printf("LambdaMetafactory 调用耗时: %.2f ms%n", (lambdaEnd - lambdaStart) / 1e6);// 准备反射方法Method reflectiveMethod = getReflectiveMethod();// 测试反射调用long reflectionStart = System.nanoTime();for (int i = 0; i < ITERATIONS; i++) {reflectiveMethod.invoke(null, "World");}long reflectionEnd = System.nanoTime();System.out.printf("反射调用耗时: %.2f ms%n", (reflectionEnd - reflectionStart) / 1e6);}
根据表格可以看出「在高频调用的场景下,LambdaMetafactory 与直接调用性能几乎相同,而反射性能几乎慢了将近四分之一」
LambdaMetafactory 工作原理
我们从核心方法 LambdaMetafactory.metafactory 生成 CallSite 作为入口进行分析其实现原理
该方法通过一系列的元数据,使用工厂来构建 CallSite
public static CallSite metafactory(MethodHandles.Lookup caller,String invokedName,MethodType invokedType,MethodType samMethodType,MethodHandle implMethod,MethodType instantiatedMethodType)throws LambdaConversionException {AbstractValidatingLambdaMetafactory mf;//工厂实例mf = new InnerClassLambdaMetafactory(caller, invokedType,invokedName, samMethodType,implMethod, instantiatedMethodType,false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);//校验参数mf.validateMetafactoryArgs();//生产CallSitereturn mf.buildCallSite();}
「InnerClassLambdaMetafactory 工厂主要为 lambda 调用点创建内部类、以及封装构建 CallSite」
buildCallSite 构建 CallSite 时:「先通过 spinInnerClass 方法创建内部类,再根据元数据在内部类中找到 MethodHandle 封装为 CallSite」
「创建内部类是通过 ASM 字节码库的 ClassWriter,将元数据写入后转换为流,最后通过 UNSAFE 类的defineAnonymousClass生成类」(逻辑代码如下)
private Class<?> spinInnerClass() throws LambdaConversionException {//元数据写入ClassWritercw.visit(CLASSFILE_VERSION, ACC_SUPER + ACC_FINAL + ACC_SYNTHETIC,lambdaClassName, null,JAVA_LANG_OBJECT, interfaces);//转为字节流final byte[] classBytes = cw.toByteArray();//UNSAFE生成return UNSAFE.defineAnonymousClass(targetClass, classBytes, null);}
总的来说,「LambdaMetafactory 实际上是使用指定的元数据,通过 ASM 字节码库动态生成内部类,通过调用内部类接口方法来间接实现调用目标方法」
这样就即实现反射调用的灵活,又能享受直接调用的性能,只是初次生成类存在一定的开销
(实际上这也是使用 Lambda 语法糖时会隐式帮助我们做的事情)
LambdaMetafactory 虽然能够带来性能优势,但也存在一定的劣势,比如:依赖接口、使用更复杂...
根据不同的应用场景可以从反射、反射工具类、LambdaMetafactory 中选择最适合的解决方案
总结
在高频使用反射的场景下,常常会有创建临时对象、软引用缓存被 gc 清空、无法使用 JIT 优化等问题而导致性能受到影响的情况
「LambdaMetafactory 带来了更加优雅的动态调用方式」,虽然会有部分生成内部类的开销,但它解决了长期以来困扰开发者的反射性能问题
「LambdaMetafactory 使用元数据通过 ASM 字节码库、Unsafe 类动态生成匿名内部类,再封装为 Methodhandler、CallSite 进行使用」
(同时它也是 Lambda 语法糖的隐式实现,对于开发者透明)
对于不同的应用场景可以选择反射、Spring ReflectionUtils、LambdaMetafactory 等多种方案进行解决问题