反射太慢了?那是你不会用LambdaMetafactory!
引言在 Java 的世界里,反射一直是开发者们既爱又恨的功能
一方面,它提供了极大的灵活性,另一方面,反射带来的性能开销却让人头疼不已
在之前的文章中,我们介绍使用 Spring 工具类ReflectionUtils通过缓存、避免创建临时对象的方式来优化反射的性能
但工具类在调用方法时依旧会使用反射的invoke,在高频调用的场景性能还是会与直接调用相差一大截
随着Java8的到来,LambdaMetafactory 为我们打开了新的大门,它「不仅能够使用类似反射的灵活性,在高频调用场景下还能与直接调用的性能相差不大」
本文将深入浅出的介绍 LambdaMetafactory,从使用、性能测试、原理、应用等方面阐述它所带来的优势
使用 LambdaMetafactoryLambdaMetafactory 是 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 获取生成的函数接口实现类再调用接口方法?定义的函数式接口
?@FunctionalInterfaceinterfaceGreeter{ voidgreet(Stringname);}
?使用 LambdaMetafactory
?publicclassLambdaMetafactoryExample{ publicstaticvoidsayHello(String name){ System.out.println("Hello, "+ name); }
publicstaticvoidmain(String[] args)throwsThrowable { // 获取 Lookup 对象 MethodHandles.Lookuplookup=MethodHandles.lookup();
// 编写方法类型 返回、入参类型 MethodTypemethodType=MethodType.methodType(void.class, String.class); // 查找目标方法句柄 MethodHandletargetMethod=lookup.findStatic(LambdaMetafactoryExample.class,"sayHello", methodType);
// 准备元数据并创建 CallSite CallSitecallSite=LambdaMetafactory.metafactory( lookup, "greet",// 要生成的lambda方法名 MethodType.methodType(Greeter.class),// 调用点签名 methodType,// 目标方法类型(注意这里应该直接是 methodType) targetMethod,// 目标方法句柄 methodType// 目标方法类型
// 获取并调用 MethodHandlefactory=callSite.getTarget(); Greetergreeter=(Greeter) factory.invokeWithArguments();
// 调用接口方法 greeter.greet("World"); }}总的来说,「LambdaMetafactory 就是通过 lambda 生成接口方法,从而实现灵活调用目标方法」
性能对比反射性能上带来的劣势主要有以下几个原因:
「调用时需要安全检查、类型转换,查找到方法、字段后会创建临时对象」「方法、字段的缓存不是强引用,gc 后会被清空」「动态运行,高频访问无法使用 JIT 优化」JVM 通过解释、编译混合运行,对于高频访问的代码会使用 JIT 将字节码转化为机器码缓存在方法区,无需再进行解释执行,在高频访问的场景下反射无法使用该优化
而 LambdaMetafactory 相比反射具有显著的性能优势,主要原因在于:
「减少开销:减少每次调用时的安全检查和类型转换等操作,使用 Lookup 进行查找」「避免重复计算:一旦 lambda 被初始化,后续调用几乎无额外开销」「即时编译:生成的代码可以被 JVM 即时编译器 (JIT) 优化」这些因素使得 LambdaMetafactory 在循环、频繁调用的场景中尤为出色
测试代码如下,也可以直接看后面的结果表格:
publicclassLambdaVsReflectionBenchmark{
// 目标方法 publicstaticvoidsayHello(String name){ name ="Hello,"+ name;// System.out.println("Hello, " + name); }
// 使用 LambdaMetafactory 创建 lambda 表达式 privatestaticGreetercreateLambda() 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(); }
// 使用反射获取目标方法 privatestaticMethodgetReflectiveMethod() throws Exception{ returnLambdaVsReflectionBenchmark.class.getMethod("sayHello", String.class); }
// 测试调用次数// private static final long ITERATIONS = 1_000L; privatestaticfinallongITERATIONS =1_000_000_000L;
/** * 调用次数:1000 * 直接调用耗时: 0.13 ms * LambdaMetafactory 调用耗时: 0.17 ms * 反射调用耗时: 1.74 ms * * 调用次数:1000000000 * 直接调用耗时: 4501.54 ms * LambdaMetafactory 调用耗时: 4640.59 ms * 反射调用耗时: 6142.39 ms * * @param args * @throws Throwable */ publicstaticvoidmain(String[] args) throws Throwable{
System.out.println("调用次数:"+ ITERATIONS); //直接调用 longstart = System.nanoTime(); for(inti =0; i ITERATIONS; i++) { sayHello("World"); } longend = System.nanoTime(); System.out.printf("直接调用耗时: %.2f ms%n", (end - start) /1e6);
// 准备 Lambda Greeter lambdaGreeter = createLambda();
// 测试 Lambda 调用 longlambdaStart = System.nanoTime(); for(inti =0; i ITERATIONS; i++) { lambdaGreeter.greet("World"); } longlambdaEnd = System.nanoTime(); System.out.printf("LambdaMetafactory 调用耗时: %.2f ms%n", (lambdaEnd - lambdaStart) /1e6);
// 准备反射方法 Method reflectiveMethod = getReflectiveMethod();
// 测试反射调用 longreflectionStart = System.nanoTime(); for(inti =0; i ITERATIONS; i++) { reflectiveMethod.invoke(null,"World"); } longreflectionEnd = System.nanoTime(); System.out.printf("反射调用耗时: %.2f ms%n", (reflectionEnd - reflectionStart) /1e6); }
直接调用LambdaMetafactory反射循环 1000 次0.13 ms0.17 ms1.74 ms循环 1000000000 次4501.54 ms4640.59 ms6142.39 ms根据表格可以看出「在高频调用的场景下,LambdaMetafactory 与直接调用性能几乎相同,而反射性能几乎慢了将近四分之一」
LambdaMetafactory 工作原理我们从核心方法LambdaMetafactory.metafactory生成 CallSite 作为入口进行分析其实现原理
该方法通过一系列的元数据,使用工厂来构建 CallSite
publicstaticCallSitemetafactory(MethodHandles.Lookup caller, String invokedName, MethodType invokedType, MethodType samMethodType, MethodHandle implMethod, MethodType instantiatedMethodType) throwsLambdaConversionException { AbstractValidatingLambdaMetafactory mf; //工厂实例 mf =newInnerClassLambdaMetafactory(caller, invokedType, invokedName, samMethodType, implMethod, instantiatedMethodType, false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY); //校验参数 mf.validateMetafactoryArgs(); //生产CallSite returnmf.buildCallSite();}「InnerClassLambdaMetafactory工厂主要为 lambda 调用点创建内部类、以及封装构建 CallSite」
buildCallSite构建 CallSite 时:「先通过spinInnerClass方法创建内部类,再根据元数据在内部类中找到 MethodHandle 封装为 CallSite」
「创建内部类是通过 ASM 字节码库的 ClassWriter,将元数据写入后转换为流,最后通过 UNSAFE 类的defineAnonymousClass生成类」(逻辑代码如下)
privateClass spinInnerClass()throwsLambdaConversionException { //元数据写入ClassWriter cw.visit(CLASSFILE_VERSION, ACC_SUPER + ACC_FINAL + ACC_SYNTHETIC, lambdaClassName,null, JAVA_LANG_OBJECT, interfaces);
//转为字节流 finalbyte[] classBytes = cw.toByteArray();
//UNSAFE生成 returnUNSAFE.defineAnonymousClass(targetClass, classBytes,null);}总的来说,「LambdaMetafactory 实际上是使用指定的元数据,通过 ASM 字节码库动态生成内部类,通过调用内部类接口方法来间接实现调用目标方法」
这样就即实现反射调用的灵活,又能享受直接调用的性能,只是初次生成类存在一定的开销
(实际上这也是使用 Lambda 语法糖时会隐式帮助我们做的事情)
LambdaMetafactory 虽然能够带来性能优势,但也存在一定的劣势,比如:依赖接口、使用更复杂...
根据不同的应用场景可以从反射、反射工具类、LambdaMetafactory 中选择最适合的解决方案
总结在高频使用反射的场景下,常常会有创建临时对象、软引用缓存被 gc 清空、无法使用 JIT 优化等问题而导致性能受到影响的情况
「LambdaMetafactory 带来了更加优雅的动态调用方式」,虽然会有部分生成内部类的开销,但它解决了长期以来困扰开发者的反射性能问题
「LambdaMetafactory 使用元数据通过 ASM 字节码库、Unsafe 类动态生成匿名内部类,再封装为 Methodhandler、CallSite 进行使用」
(同时它也是 Lambda 语法糖的隐式实现,对于开发者透明)
对于不同的应用场景可以选择反射、Spring ReflectionUtils、LambdaMetafactory等多种方案进行解决问题
AI编程资讯AI Coding专区指南:https://aicoding.juejin.cn/aicoding
点击"阅读原文"了解详情~
阅读原文
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。 项目经理在线