JDK JDK 学习手册8 → 27 · JDK 专用学习站
DEEPDIVE · LANGUAGE · JEP 126

Lambda 的字节码真相:invokedynamic 与 LambdaMetafactory

JDK 8 · 深钻主题 01 / 03 · 预计 50 分钟
深入四步深钻JEP 126
一句话Lambda 不是匿名类的语法糖。编译期,Lambda 体被提取成私有方法,调用点变成 invokedynamic;运行期,LambdaMetafactory 首次链接时生成实现函数式接口的类。
本质:Lambda 的「代码」在编译期变成一个私有方法;「对象」在运行期由 call site 首次链接时生成。调用点只链接一次,之后复用。

学完你要能自己回答

  1. 为什么 javac 不把 Lambda 编译成匿名内部类?
  2. invokedynamic 的 bootstrap 参数分别是什么?LambdaMetafactory 如何用它们生成类?
  3. 捕获变量存在哪里?为什么 Lambda 的 this 指向外部类?
  4. Lambda 的性能与序列化边界在哪里?
01 Lambda 被拆成「方法」和「调用点」02 this、作用域与 effectively final

Lambda 被拆成「方法」和「调用点」01

javac 不为每个 Lambda 生成一个内部类。它把 Lambda 体提取成一个私有静态方法(捕获变量作为额外参数),并在原位置生成一条 invokedynamic 指令。方法体是编译期确定的;实现接口的对象是运行期链接时生成的。

匿名内部类Lambda
编译产物单独的 .class 文件invokedynamic + 私有方法
对象创建每次 new首次链接生成 CallSite,之后复用
this指向匿名类实例指向外部类实例
捕获构造器参数 / 合成字段方法参数 + 生成类字段
调试类名可读合成方法 lambda$...

「Lambda 是匿名类的语法糖」只在行为层面成立;字节码层面两者完全不同。匿名类在编译期就确定了类和构造器,Lambda 的类要等到运行期第一次执行到调用点才生成。

这也解释了为什么 Lambda 的 getClass() 得到的是 Host$$Lambda$1/0x... 这样的合成类,而不是源码里某个内部类。

this、作用域与 effectively final02

Lambda 没有自己的 this,所以 this.name 访问的是外部实例;匿名内部类有自己的 this。这也是 Lambda 不能访问自身成员、只能访问外部实例成员的原因。

public class ThisDemo {
    String name = "outer";

    void test() {
        Runnable lambda = () -> System.out.println(this.name);
        Runnable anon = new Runnable() {
            String name = "anon";
            public void run() { System.out.println(this.name); }
        };
        lambda.run();
        anon.run();
    }

    public static void main(String[] args) {
        new ThisDemo().test();
    }
}
outer
anon

Lambda 只能捕获 effectively final 的局部变量。捕获的是值:引用类型捕获引用,基本类型捕获当时的值;之后外部再改字段,Lambda 看到的是字段的新值,因为它捕获的是 this,不是字段快照。

在 Lambda 里修改外部局部变量会编译失败:local variables referenced from a lambda expression must be final or effectively final。
01 invokedynamic:链接一次,调用多次02 生成类:实现接口,转发到私有方法03 捕获变量:作为字段存进生成类

invokedynamic:链接一次,调用多次01

普通方法调用在常量池里指向一个方法符号引用;invokedynamic 指向一个 bootstrap method 和一组静态参数。第一次执行时,JVM 调用 bootstrap 生成 CallSite,把目标方法句柄装进去;之后的调用直接走链接好的 target。

import java.util.function.Function;

public class LambdaSite {
    public static void main(String[] args) {
        Function<String, Integer> f = s -> s.length();
        System.out.println(f.apply("jdk8"));
    }
}
javap -c -p LambdaSite.class
  public static void main(java.lang.String[]);
    Code:
       0: invokedynamic #2,  0              // InvokeDynamic #0:apply:()Ljava/util/function/Function;
       5: astore_1

invokedynamic 后面没有直接的方法地址,只有 bootstrap 序号和返回类型。apply:()Ljava/util/function/Function; 表示这个调用点要产出一个 Function 实例。

javap -v -p LambdaSite.class
    Method arguments:
      #24 (Ljava/lang/Object;)Ljava/lang/Object;
      #25 invokestatic LambdaSite.lambda$main$0:(Ljava/lang/String;)Ljava/lang/Integer;
      #26 (Ljava/lang/String;)Ljava/lang/Integer;
参数本例中的值作用
Lookup调用者上下文访问权限与类加载
nameapply实现的接口方法名
invokedType() -> Function调用点类型
samMethodType(Object)Object接口的擦除签名
implMethodlambda$main$0真正执行的方法
instantiatedMethodType(String)Integer泛型特化后的签名
bootstrap 只在首次链接时执行;链接失败会抛 BootstrapMethodError 或 LambdaConversionException,不是普通的方法调用异常。

生成类:实现接口,转发到私有方法02

用 -Djdk.internal.lambda.dumpProxyClasses=. 可以把运行期生成的类落盘。它实现目标接口,方法体把参数转发给编译期提取的私有方法。

javac LambdaSite.java
java -Djdk.internal.lambda.dumpProxyClasses=. LambdaSite
javap -p 'LambdaSite$$Lambda$1.class'
final class LambdaSite$$Lambda$1 implements java.util.function.Function {
  private LambdaSite$$Lambda$1();
  public java.lang.Object apply(java.lang.Object);
}

生成类的名字包含 $$Lambda$ 和序号,序号依赖链接顺序;不要用类名或 getClass() 做业务判断。它由 LambdaMetafactory 在运行期定义,属于合成类。

生成类会增加 Metaspace 占用;大量不同调用点(例如脚本动态生成 Lambda)会累积类,必要时用 -XX:MaxMetaspaceSize 兜底。

捕获变量:作为字段存进生成类03

有捕获时,bootstrap 的静态参数只描述签名;真正的捕获值在每次执行 invokedynamic 时作为参数传给生成的工厂,工厂再存进生成类的字段。

import java.util.function.Function;

public class CaptureSite {
    public static void main(String[] args) {
        String prefix = "jdk";
        int n = 8;
        Function<String, String> f = s -> prefix + s + n;
        System.out.println(f.apply("-"));
    }
}
jdk-8
java -Djdk.internal.lambda.dumpProxyClasses=. CaptureSite
javap -p 'CaptureSite$$Lambda$1.class'
final class CaptureSite$$Lambda$1 implements java.util.function.Function {
  private final java.lang.String arg$1;
  private final int arg$2;
  private CaptureSite$$Lambda$1(java.lang.String, int);
  private static java.util.function.Function get$Lambda(java.lang.String, int);
  public java.lang.Object apply(java.lang.Object);
}

所以捕获是「值传递」:基本类型存值,引用类型存引用。effectively final 保证这个快照在 Lambda 生命周期内不会被重新赋值,避免语义歧义。

可序列化 Lambda 需要写成 (Function<String,String> & Serializable) 这样的交叉类型;生成类会增加 writeReplace/$deserializeLambda$,反序列化依赖原类的实现方法,跨版本很脆弱,不建议用于持久化格式。
01 怎么读一个 Lambda 的字节码02 性能与使用边界

怎么读一个 Lambda 的字节码01

  1. javap -c -p Host.class:在方法里找 invokedynamic,记下 bootstrap 序号。
  2. javap -v -p Host.class:在 BootstrapMethods 里找对应序号,看 implMethod 指向哪个 lambda$... 方法。
  3. java -Djdk.internal.lambda.dumpProxyClasses=.:把生成类落盘,确认它实现的接口与转发目标。
  4. 对照 implMethod 的签名与 SAM 签名,检查是否有装箱、强制转换或参数顺序问题。
javap -c -p LambdaSite.class
javap -v -p LambdaSite.class | grep -A5 BootstrapMethods
如果运行期报 AbstractMethodError 或 LambdaConversionException,优先检查编译期与运行期的接口签名是否一致,以及字节码增强工具是否改坏了 invokedynamic。

性能与使用边界02

Lambda 的调用点只链接一次,之后 JIT 可以把生成类的方法内联到调用方;因此「Lambda 比匿名类慢」不是一个稳定结论。真正的成本在首次链接、生成类,以及装箱。

  • 热点路径优先用 IntFunction/ToIntFunction 等特化接口,避免 Function<Integer,Integer> 的装箱。
  • 不要在 Lambda 里做昂贵初始化;无捕获 Lambda 也可能在每次调用点执行时创建对象。
  • 不要依赖 Lambda 的实例身份或类名;不同调用点、不同 JVM 实现都不保证相同。
  • 受检异常要在 Lambda 内处理或封装;不要把 throws 藏在 RuntimeException 里逃避设计。
  • 大量动态调用点会累积生成类,占用 Metaspace;脚本化场景要限制调用点数量。
Lambda 适合表达短小的行为;超过五六行、需要多个返回值或复杂控制流时,写成具名方法更利于调试和复用。
01 先猜再看02 能否讲清因果

先猜再看01

先猜再看Lambda 编译后是一个内部类吗?
不是。编译期生成 invokedynamic 和私有静态方法;实现接口的类是运行期由 LambdaMetafactory 生成的。
先猜再看捕获的局部变量存在栈上还是对象里?
链接时作为参数传给生成类的工厂,存进生成类的字段;捕获的是值,不是变量槽。
先猜再看为什么 Lambda 里的 this 是外部类?
Lambda 没有独立的接收者。invokedynamic 生成的方法直接调用编译期提取的静态方法,静态方法没有 this;访问到的 this 来自外部实例的捕获。

能否讲清因果02

01. 说出 invokedynamic 与 invokevirtual 的区别。
invokevirtual 在常量池里直接指向方法符号引用;invokedynamic 指向 bootstrap method 和静态参数,首次执行时链接出 CallSite,之后调用 target。它把「选哪个实现」推迟到运行期。
02. BootstrapMethods 里的 implMethod 是什么?
它是编译期提取的私有静态方法,包含 Lambda 的实际逻辑;捕获变量会作为额外参数传入。生成类的 apply 最终转发到它。
03. 为什么捕获变量必须 effectively final?
捕获的是值快照。如果之后还能重新赋值,Lambda 内外看到的值会不一致;effectively final 保证快照稳定。
04. 非捕获 Lambda 的生成类有哪些成员?
实现目标接口、无实例字段、私有构造器、转发方法;具体是否还有工厂方法取决于 javac 实现,不要依赖。
05. 如何验证一个 Lambda 没有生成匿名内部类?
javap -c -p 看到 invokedynamic,源码目录没有对应 class;再用 -Djdk.internal.lambda.dumpProxyClasses 看运行期生成类。
能把「编译期方法 + 运行期调用点」这句话讲清楚,并说清捕获值放在哪里、this 为什么是外部类,这一节就过关了。
学习进度 记录保存在本机浏览器 · 不上传

JDK 学习手册 · JDK / Java 专用学习站 · 纯静态 · 双击 index.html 即可使用

本站专注 JDK 版本演进与 Java 教学,非 Oracle / OpenJDK 官方站点。版本数据来自 OpenJDK 官方 Features 表;课程示例已在对应本机 JDK 实测。构建日期 2026-11-12。