Lambda 的字节码真相:invokedynamic 与 LambdaMetafactory
学完你要能自己回答
- 为什么 javac 不把 Lambda 编译成匿名内部类?
- invokedynamic 的 bootstrap 参数分别是什么?LambdaMetafactory 如何用它们生成类?
- 捕获变量存在哪里?为什么 Lambda 的 this 指向外部类?
- Lambda 的性能与序列化边界在哪里?
Lambda 被拆成「方法」和「调用点」01
javac 不为每个 Lambda 生成一个内部类。它把 Lambda 体提取成一个私有静态方法(捕获变量作为额外参数),并在原位置生成一条 invokedynamic 指令。方法体是编译期确定的;实现接口的对象是运行期链接时生成的。
| 匿名内部类 | Lambda | |
|---|---|---|
| 编译产物 | 单独的 .class 文件 | invokedynamic + 私有方法 |
| 对象创建 | 每次 new | 首次链接生成 CallSite,之后复用 |
| this | 指向匿名类实例 | 指向外部类实例 |
| 捕获 | 构造器参数 / 合成字段 | 方法参数 + 生成类字段 |
| 调试 | 类名可读 | 合成方法 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,不是字段快照。
local variables referenced from a lambda expression must be final or effectively final。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 | 调用者上下文 | 访问权限与类加载 |
| name | apply | 实现的接口方法名 |
| invokedType | () -> Function | 调用点类型 |
| samMethodType | (Object)Object | 接口的擦除签名 |
| implMethod | lambda$main$0 | 真正执行的方法 |
| instantiatedMethodType | (String)Integer | 泛型特化后的签名 |
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 在运行期定义,属于合成类。
-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 生命周期内不会被重新赋值,避免语义歧义。
(Function<String,String> & Serializable) 这样的交叉类型;生成类会增加 writeReplace/$deserializeLambda$,反序列化依赖原类的实现方法,跨版本很脆弱,不建议用于持久化格式。怎么读一个 Lambda 的字节码01
javap -c -p Host.class:在方法里找invokedynamic,记下 bootstrap 序号。javap -v -p Host.class:在BootstrapMethods里找对应序号,看implMethod指向哪个lambda$...方法。java -Djdk.internal.lambda.dumpProxyClasses=.:把生成类落盘,确认它实现的接口与转发目标。- 对照
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;脚本化场景要限制调用点数量。
先猜再看01
先猜再看Lambda 编译后是一个内部类吗?
invokedynamic 和私有静态方法;实现接口的类是运行期由 LambdaMetafactory 生成的。先猜再看捕获的局部变量存在栈上还是对象里?
先猜再看为什么 Lambda 里的 this 是外部类?
invokedynamic 生成的方法直接调用编译期提取的静态方法,静态方法没有 this;访问到的 this 来自外部实例的捕获。能否讲清因果02
01. 说出 invokedynamic 与 invokevirtual 的区别。
02. BootstrapMethods 里的 implMethod 是什么?
03. 为什么捕获变量必须 effectively final?
04. 非捕获 Lambda 的生成类有哪些成员?
05. 如何验证一个 Lambda 没有生成匿名内部类?
javap -c -p 看到 invokedynamic,源码目录没有对应 class;再用 -Djdk.internal.lambda.dumpProxyClasses 看运行期生成类。