稀土掘金技术社区

反射太慢了?那是你不会用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 的目标方法句柄进行调用

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

  1. 使用 Lookup 查找要调用的目标方法句柄 MethodHandle
  2. 根据要生成的 lambda 方法名、调用接口、目标方法类型(返回、入参类型)、句柄等信息创建 CallSite
  3. 根据 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);
        // 准备元数据并创建 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 {
    // 目标方法    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);
        // 准备 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 等多种方案进行解决问题