Java Lambda 底层实现

5 阅读1分钟

lambda不像匿名内部类生成一大堆的.class文件,他的底层实现原理主要如下,分为编译,运行,首次调用等过程

例如有这样一段代码:

Runnable task = () -> System.out.println("hello");
task.run();

在编译阶段,javac 首先会把 Lambda 表达式中的方法体抽取出来,放到当前类中,生成一个合成方法

//static类主要用于后续适配器的调用
private static void lambda$main$0() {
    System.out.println("hello");
}

所以 Lambda 中真正的业务代码,在编译期就已经被编译成普通字节码,并保存在当前类的 .class 文件中了。

但是程序还需要得到一个 Runnable 对象,否则后面的 task.run() 无法执行。对于匿名内部类,编译器会直接生成一个 LambdaDemo$1.class,然后在代码中执行 new LambdaDemo$1()。Lambda 没有这么做,它会把创建 Runnable 对象的位置编译成一条 invokedynamic 指令。

invokedynamic run:()Ljava/lang/Runnable;

这个字节码的作用不是直接执行 Lambda 方法体,而是负责在程序第一次运行到这里时,确定这个位置以后怎样创建 Runnable 对象。

除了生成 invokedynamic 指令,javac 还会在当前类的 BootstrapMethods 属性中记录一份 Lambda 的描述信息。这里面会说明目标函数式接口是 Runnable,接口方法是 run(),真正的实现方法是 LambdaDemo.lambda$main$0()引导方法则是 LambdaMetafactory.metafactory()。当 JVM 第一次执行到这条 invokedynamic 指令时就会调用这个引导方法,并把 Runnablerun()lambda$main$0() 等信息传给它。

LambdaMetafactory 的任务,就是为这个 invokedynamic 调用点构造一个 CallSite。这个 CallSite 可以简单理解为:以后程序再执行到这个位置时,具体应该调用哪段逻辑来得到 Lambda 对象。

在 JDK 8 的具体实现中,LambdaMetafactory 最终通过 ASM 在内存中动态生成一个类,这个类的逻辑大致相当于:

final class LambdaDemo$$Lambda$1 implements Runnable {

    @Override
    public void run() {
        LambdaDemo.lambda$main$0();
    }
}

这个类本身并不包含真正的业务逻辑,它主要是一个适配器。它负责实现 Runnable 接口,并把 run() 方法转发到编译期已经生成的 lambda$main$0() 方法。

这个class文件不会和匿名内部类一样写入磁盘而是 InnerClassLambdaMetafactory 在程序运行过程中,通过 ASM 生成一个 byte[]。这个字节数组的内容符合 class 文件格式,但它只存在于内存中,不会默认写成 LambdaDemo$$Lambda$1.class 文件。

当动态类生成完毕以后,LambdaMetafactory 会返回一个已经绑定好的 CallSite。以后再次执行同一个 invokedynamic 调用点时,就不需要重新调用 LambdaMetafactory,也不需要重新生成实现类,而是直接使用已经链接好的 CallSite 创建或者返回 Lambda 对象。

对于没有捕获外部变量的 Lambda,生成的实现类不需要保存任何字段,因此运行时通常可以创建一个对象并重复使用。此时执行 invokedynamic,通常就是返回这个已经创建好的 Runnable 实例。

**lambda如何捕获外部变量?**实际上就是通过参数传递进去,如下,对于捕获了外部变量的 Lambda:

int value = 10;
Runnable task = () -> System.out.println(value);

编译器会把 Lambda 方法体转换成类似下面的方法:

private static void lambda$main$0(int value) {
    System.out.println(value);
}

当执行到invokedynamic时候会把外部变量复制一份

运行时生成的实现类则需要增加字段,用来保存捕获到的 value

final class LambdaDemo$$Lambda$1 implements Runnable {

    private final int arg$1;

    LambdaDemo$$Lambda$1(int value) {
        this.arg$1 = value;
    }

    @Override
    public void run() {
        LambdaDemo.lambda$main$0(arg$1);
    }
}

这种情况下,Lambda 实现类通常仍然只生成一次,但每次执行 Lambda 创建表达式时,都可能创建一个新的对象实例,并把当前捕获到的值存入对象字段。

最终执行 task.run() 时,这已经是一次普通的接口方法调用。它先调用运行时生成类的 run(),然后再由这个 run() 转发到 LambdaDemo.lambda$main$0(),真正执行 Lambda 中的业务代码。代码成为热点以后,JIT 还可能把中间的接口调用、适配器方法和合成方法全部内联,最终直接生成对应的机器码。

所以整个过程可以概括为:javac 在编译期把 Lambda 方法体生成普通方法,同时在创建 Lambda 对象的位置生成 invokedynamic;第一次执行 invokedynamic 时,JVM 调用 LambdaMetafactoryLambdaMetafactory 在运行时生成一个实现函数式接口的类,并将其直接定义到 JVM 中;invokedynamic 返回这个类的对象;调用函数式接口方法时,再转发到编译期生成的 Lambda 合成方法。