反射太慢?那是你不会用LambdaMetafactory!

举报
菜菜的后端私房菜 发表于 2024/12/25 15:54:25 2024/12/25
567 0 0
【摘要】 本文将深入浅出的介绍LambdaMetafactory,从使用、性能测试、原理、应用等方面阐述它所带来的优势

反射太慢?那是你不会用LambdaMetafactory!

在Java的世界里,反射一直是开发者们既爱又恨的功能

一方面,它提供了极大的灵活性,另一方面,反射带来的性能开销却让人头疼不已

之前的文章中,我们介绍使用Spring工具类ReflectionUtils通过缓存、避免创建临时对象的方式来优化反射的性能

但工具类在调用方法时依旧会使用反射的invoke,在高频调用的场景性能还是会与直接调用相差一大截

随着Java 8的到来,LambdaMetafactory为我们打开了新的大门,它不仅能够使用类似反射的灵活性,在高频调用场景下还能与直接调用的性能相差不大

本文将深入浅出的介绍LambdaMetafactory,从使用、性能测试、原理、应用等方面阐述它所带来的优势(导图如下)

导图

使用LambdaMetafactory

LambdaMetafactory是Java 8引入的一个类,位于java.lang.invoke包下

它的主要任务是在运行时创建lambda表达式的实现

LambdaMetafactory通过MethodHandle和CallSite机制工作,它能够在运行时通过ASM字节码库生成lambda内部类来调用目标方法,从而兼得反射的灵活性与直接调用的性能优势

MethodHandle是方法句柄,通过它可以灵活调用目标方法;CallSite用于动态管理存储方法句柄MethodHandle,它们是实现动态调用的关键

LambdaMetafactory的核心在于其LambdaMetafactory.metafactory方法

该方法根据目标方法、接口方法等数据来定义目标lambda的行为,并返回一个CallSite对象

后续通过获取CallSite的目标方法句柄进行调用

使用方法通常分为以下几个步骤:

  1. 使用Lookup查找要调用的目标方法句柄MethodHandle
  2. 根据要生成的lambda方法名、调用接口、目标方法类型(返回、入参类型)、句柄等信息创建CallSite
  3. 根据CallSite的MethodHandle获取生成的函数接口实现类再调用接口方法

定义的函数式接口

@FunctionalInterface
interface 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);

        // 准备元数据并创建 CallSite
        CallSite 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生成接口方法,从而实现灵活调用目标方法

性能对比

反射性能上带来的劣势主要有以下几个原因:

  1. 调用时需要安全检查、类型转换,查找到方法、字段后会创建临时对象
  2. 方法、字段的缓存不是强引用,gc后会被清空
  3. 动态运行,高频访问无法使用JIT优化

JVM通过解释、编译混合运行,对于高频访问的代码会使用JIT将字节码转化为机器码缓存在方法区,无需再进行解释执行,在高频访问的场景下反射无法使用该优化

而LambdaMetafactory相比反射具有显著的性能优势,主要原因在于:

  1. 减少开销:减少每次调用时的安全检查和类型转换等操作,使用Lookup进行查找
  2. 避免重复计算:一旦lambda被初始化,后续调用几乎无额外开销
  3. 即时编译:生成的代码可以被JVM即时编译器(JIT)优化

这些因素使得LambdaMetafactory在循环、频繁调用的场景中尤为出色

测试代码如下,也可以直接看后面的结果表格:

public class LambdaVsReflectionBenchmark {

    // 目标方法
    public static void sayHello(String name) {
        name = "Hello," + name;
//        System.out.println("Hello, " + name);
    }

    // 使用 LambdaMetafactory 创建 lambda 表达式
    private static 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();
    }

    // 使用反射获取目标方法
    private static 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
     */
    public static void main(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);

        // 准备 Lambda
        Greeter 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 反射
循环1000次 0.13 ms 0.17 ms 1.74 ms
循环1000000000次 4501.54 ms 4640.59 ms 6142.39 ms

根据表格可以看出在高频调用的场景下,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();
    //生产CallSite
    return mf.buildCallSite();
}

InnerClassLambdaMetafactory 工厂主要为lambda调用点创建内部类、以及封装构建CallSite

buildCallSite 构建CallSite时:先通过 spinInnerClass 方法创建内部类,再根据元数据在内部类中找到MethodHandle封装为CallSite

创建内部类是通过ASM字节码库的ClassWriter,将元数据写入后转换为流,最后通过UNSAFE类的defineAnonymousClass生成类(逻辑代码如下)

private Class<?> spinInnerClass() throws LambdaConversionException {
    //元数据写入ClassWriter 
    cw.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等多种方案进行解决问题

最后(一键三连求求拉~)

😊我是菜菜,热爱技术交流、分享与写作,喜欢图文并茂、通俗易懂的输出知识

📚在我的博客中,你可以找到Java技术栈的各个专栏:Java并发编程与JVM原理、Spring和MyBatis等常用框架及Tomcat服务器的源码解析,以及MySQL、Redis数据库的进阶知识,同时还提供关于消息中间件和Netty等主题的系列文章,都以通俗易懂的方式探讨这些复杂的技术点

🏆除此之外,我还是掘金优秀创作者、腾讯云年度影响力作者、华为云年度十佳博主…

👫我对技术交流、知识分享以及写作充满热情,如果你愿意,欢迎加我一起交流(vx:CaiCaiJava666),也可以持续关注我的公众号:菜菜的后端私房菜,我会分享更多技术干货,期待与更多志同道合的朋友携手并进,一同在这条充满挑战与惊喜的技术之旅中不断前行

🤝如果觉得菜菜写的不错,可以点赞、关注、收藏支持一下~

📖本篇文章被收入专栏 Java,感兴趣的同学可以持续关注喔

📝本篇文章笔记以及案例被收入 Gitee-CaiCaiJavaGithub-CaiCaiJava,除此之外还有更多Java进阶相关知识,感兴趣的同学可以star持续关注喔~

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

作者其他文章

评论(0

抱歉,系统识别当前为高风险访问,暂不支持该操作

    全部回复

    上滑加载中

    设置昵称

    在此一键设置昵称,即可参与社区互动!

    *长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

    *长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。