从Lambda底层实现一窥invokedynamic魅力

Java 8引入Lambda表达式替代匿名内部类,底层通过invokedynamic指令动态生成实现类。Lambda捕获变量时生成带参构造方法,无状态Lambda优化为单例。序列化通过SerializedLambda实现,反序列化时重建Lambda实例。性能测试显示Lambda调用效率接近原生代码。应用场景包括动态Getter/Setter、ORM框架方法引用解析等。invokedynamic还支撑了字符串拼接优化、switch模式匹配等特性。

文章目录

从匿名内部类到Lambda表达式的演进

自Java 8发布以来,Lambda表达式以其简洁的语法,迅速取代了匿名内部类在大多数场景下的应用。一个典型的例子便是创建线程:

// JDK8之前
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("Thread running...");
}
};
new Thread(r).start();
// JDK8及之后
Runnable r = () -> System.out.println("Thread running...");
new Thread(r).start();

代码量的锐减和可读性的提升,曾让笔者自然地认为,Lambda表达式或许只是Java为实现函数式接口而提供的一种“语法糖”,其底层本质仍然是我们熟悉的匿名内部类。

然而,当我们尝试编译包含Lambda表达式的类MyClass.java后,在编译产物目录中,我们却没有找到预想中的MyClass$1.class这样的匿名内部类文件。这一发现打破了我们的猜想,并引出一个核心问题:

如果不是匿名内部类,JVM究竟是如何实现Lambda的?

invokedynamic——动态调用的基石

为了探寻真相,我们可以深入字节码层面,看看JDK把lambda表达式编译成了什么。以下所有示例均基于Eclipse Temurin JDK 24.0.2+12

通过javap -v -p MyClass.class命令,我们得到了以下输出(截取部分):

{
public study.invokedynamic.MyClass();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lstudy/invokedynamic/MyClass;
public static void main();
descriptor: ()V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=1, args_size=0
0: invokedynamic #7, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
5: astore_0
6: new #11 // class java/lang/Thread
9: dup
10: aload_0
11: invokespecial #13 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
14: invokevirtual #16 // Method java/lang/Thread.start:()V
17: return
LineNumberTable:
line 8: 0
line 9: 6
line 10: 17
LocalVariableTable:
Start Length Slot Name Signature
6 12 0 r Ljava/lang/Runnable;
private static void lambda$main$0();
descriptor: ()V
flags: (0x100a) ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #19 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #25 // String Thread running...
5: invokevirtual #27 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 8: 0
}
SourceFile: "MyClass.java"
BootstrapMethods:
0: #51 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#47 ()V
#48 REF_invokeStatic study/invokedynamic/MyClass.lambda$main$0:()V
#47 ()V

通过上面的字节码,我们可以发现一些端倪:

  • main方法中没有看到直接创建Runnable实例的逻辑,而是出现了一个陌生的指令:invokedynamic
  • Lambda方法体中打印Thread running...的逻辑被放在了一个lambda$main$0方法中
  • 有一个特殊的BootstrapMethods引用了lambda$main$0方法

首先介绍一下invokedynamic(下文简称indy)指令的背景:Java虚拟机规范中定义了五种方法调用字节码指令,其中invokestaticinvokespecialinvokevirtualinvokeinterface这四种指令的调用目标在编译期就已经确定。而从JDK7开始才加入的indy则截然不同,它是一种 运行时动态绑定 的指令。当JVM首次执行到indy指令时,它不会直接去调用目标方法,而是会先调用一个被称为 “引导方法(Bootstrap Method)” 的特殊方法。此方法的返回值是一个CallSite(调用点)对象,该对象才真正链接到我们最终要执行的目标方法。后续再执行到此indy指令时,便会直接使用这个已绑定的CallSite,无需再次“引导”。

不了解相关概念的读者可能会被这段话绕晕,我们先简单介绍indy工作流中涉及的几个核心概念:

  • 方法句柄(Method Handle): java.lang.invoke.MethodHandle,可以理解为对一个方法(构造器、静态或实例方法)的直接、可执行的引用,类似于C语言中的函数指针。它是indy机制的核心承载者。
  • 调用点(Call Site): java.lang.invoke.CallSite,可以看作是方法句柄的容器,它负责将一个indy指令与一个具体的方法句柄进行“链接”。
  • 引导方法(Bootstrap Method,下称BSM): 可以理解为一个特殊的静态方法,在invokedynamic指令首次执行时被调用,它的职责是创建并返回一个调用点(CallSite)对象,用于将该指令动态链接到具体的目标方法上。

那么在深入细节之前,我们先抛出一个疑问:为什么Java团队要舍弃成熟的匿名内部类方案,转而使用复杂的indy机制来实现Lambda表达式呢?

带着疑问,我们来一步步揭开Lambda的底层实现。

循序渐进,一步步揭开Lambda的底层实现

从无状态lambda开始

通过观察上文示例的字节码,我们可以看到indy指令指向的BSM是java.lang.invoke.LambdaMetafactory#metafactory。阅读该方法的注释,我们得知它的功能是:“通过委托给一个提供的方法句柄,并在进行适当的类型适配和参数校验后,实现一个或多个接口。通常用作invokedynamic调用点的引导方法,以支持Java编程语言中的lambda表达式和方法引用表达式特性。”

现在,我们先从不捕获外部变量的Lambda例子看起:

public class LambdaTest1 {
public static void main() {
// 例子1: 常规Lambda
Runnable r1 = () -> System.out.println("Lambda 1");
r1.run();
// 例子2: 方法引用
Runnable r2 = LambdaTest1::printMessage;
r2.run();
}
private static void printMessage() {
System.out.println("Lambda 2");
}
}

通过字节码来分析Lambda表达式的运行过程未免有点过于抽象,我们不妨换种思路:

目前我们已经得知Lambda表达式是通过indy指令实现的,而indy指令首次执行会调用对应BSM,对应上文的分析这个BSM为java.lang.invoke.LambdaMetafactory#metafactory方法,那么我们可以通过断点调试来分析:

直接将断点打到metafactory方法中

可以看到,metafactory方法定义了6个参数,其中前三个是BSM规范中的固定参数:

  • MethodHandles.Lookup caller:代表调用者上下文信息和访问权限的Lookup对象
  • String interfaceMethodNameindy指令要动态调用的方法名字符串。对于Lambda表达式,这个名字就是它所实现的函数式接口的抽象方法名(例如,RunnablerunFunctionapply)。
  • MethodType factoryType:代表indy指令要动态调用的方法签名。对于Lambda表达式,这个签名通常是生成Lambda实例的工厂方法的类型(例如,返回一个Runnable,不接收任何参数)。

metafactory方法的后三个参数,则是为了实现Lambda表达式而需要的额外参数:

  • MethodType interfaceMethodType:描述函数式接口中抽象方法的方法签名。需要注意的是对于泛型参数需要擦除类型(例如,对于Runnable的方法签名为无参、无返回:()void,对于Function<String, Integer>则需要擦除泛型:(Object)Object
  • MethodHandle implementation:指向Lambda体代码实现或方法引用的方法句柄。对于我们的例子1,它指向了一个静态私有方法:lambda$main$0,这符合我们前面在字节码中观察到的现象;而对于例子2,它直接指向了LambdaTest1#printMessage方法。这里就能看出Lambda与方法引用的区别,对于Lambda,编译器需要将其代码提取到一个方法中,以供方法句柄引用。
  • MethodType dynamicMethodType:描述了调用Lambda实例时 实际的方法签名。它与第四个参数interfaceMethodType的区别在于它需要匹配实际调用的方法签名,所以这里不需要擦除泛型,对于Function<String, Integer>是:(String)Integer

了解了这些参数,metafactory的逻辑就不难看懂了:

首先创建了一个InnerClassLambdaMetafactory实例,从名字不难看出JDK最终还是通过内部类来实现Lambda表达式。

然后调用validateMetafactoryArgs方法校验上述的参数,包括参数数量是否匹配、类型是否匹配(支持自动装箱拆箱),返回值是否匹配(无返回值的函数式接口可以适配带返回值的方法实现)。

最后通过buildCallSite方法来构建调用点,Lambda的主要实现逻辑就在这个方法中:

@Override
CallSite buildCallSite() throws LambdaConversionException {
// 获取Lambda实现类
final Class<?> innerClass = spinInnerClass();
// disableEagerInitialization默认为false,这个开关的作用是控制无状态Lambda是否懒加载生成单例对象
if (factoryType.parameterCount() == 0 && disableEagerInitialization) {
try {
return new ConstantCallSite(caller.findStaticGetter(innerClass, LAMBDA_INSTANCE_FIELD,
factoryType.returnType()));
} catch (ReflectiveOperationException e) {
throw new LambdaConversionException(
"Exception finding " + LAMBDA_INSTANCE_FIELD + " static field", e);
}
} else {
try {
// 获取Lambda实现类的构造方法,构造方法的参数数量代表了捕获外部变量的数量
MethodHandle mh = caller.findConstructor(innerClass, constructorType);
if (factoryType.parameterCount() == 0) {
// 如果Lambda没有捕获外部变量,则使用该构造方法创建一个Lambda实例,并构建一个调用点,使其固定返回这个实例
Object inst = mh.invokeBasic();
return new ConstantCallSite(MethodHandles.constant(interfaceClass, inst));
} else {
// 如果捕获了外部变量,则只能每次调用都创建一个Lambda实例
return new ConstantCallSite(mh.asType(factoryType));
}
} catch (ReflectiveOperationException e) {
throw new LambdaConversionException("Exception finding constructor", e);
} catch (Throwable e) {
throw new LambdaConversionException("Exception instantiating lambda object", e);
}
}
}

现在,我们可以部分回答之前的问题了:为什么不直接用匿名内部类? 因为对于无状态Lambda,每次调用匿名内部类都会new一个新对象,而indy+LambdaMetafactory机制可以实现单例复用,极大地提升了性能,减少了不必要的GC压力。

继续往下看,查看spinInnerClass方法是如何创建Lambda实现类的:

private Class<?> spinInnerClass() throws LambdaConversionException {
// CDS does not handle disableEagerInitialization or useImplMethodHandle
if (!disableEagerInitialization && !useImplMethodHandle) {
// 可以看到,这里是利用CDS机制缓存了生成的类,以便于其他Java进程使用
if (CDS.isUsingArchive()) {
// load from CDS archive if present
Class<?> innerClass = LambdaProxyClassArchive.find(targetClass,
interfaceMethodName,
factoryType,
interfaceMethodType,
implementation,
dynamicMethodType,
isSerializable,
altInterfaces,
altMethods);
if (innerClass != null) return innerClass;
}
// include lambda proxy class in CDS archive at dump time
if (CDS.isDumpingArchive()) {
Class<?> innerClass = generateInnerClass();
LambdaProxyClassArchive.register(targetClass,
interfaceMethodName,
factoryType,
interfaceMethodType,
implementation,
dynamicMethodType,
isSerializable,
altInterfaces,
altMethods,
innerClass);
return innerClass;
}
}
// 核心的机制都在下面这个方法内
return generateInnerClass();
}
private Class<?> generateInnerClass() throws LambdaConversionException {
List<ClassDesc> interfaces;
ClassDesc interfaceDesc = classDesc(interfaceClass);
// 检查是否实现Serializable接口
boolean accidentallySerializable = !isSerializable && Serializable.class.isAssignableFrom(interfaceClass);
if (altInterfaces.length == 0) {
interfaces = List.of(interfaceDesc);
} else {
// 由于一个接口也可能继承多个其他接口,这些接口都需要被Lambda实现类实现
Set<ClassDesc> itfs = LinkedHashSet.newLinkedHashSet(altInterfaces.length + 1);
itfs.add(interfaceDesc);
for (Class<?> i : altInterfaces) {
itfs.add(classDesc(i));
accidentallySerializable |= !isSerializable && Serializable.class.isAssignableFrom(i);
}
interfaces = List.copyOf(itfs);
}
final boolean finalAccidentallySerializable = accidentallySerializable;
// 可以看到,JDK24的实现中,使用了最新的ClassFile API来生成Lambda实现类
final byte[] classBytes = ClassFile.of().build(lambdaClassEntry, pool, new Consumer<ClassBuilder>() {
@Override
public void accept(ClassBuilder clb) {
clb.withFlags(ACC_SUPER | ACC_FINAL | ACC_SYNTHETIC)
.withInterfaceSymbols(interfaces);
// 如果Lambda捕获了外部变量,则需要通过构造方法传入
for (int i = 0; i < argDescs.length; i++) {
clb.withField(argName(i), argDescs[i], ACC_PRIVATE | ACC_FINAL);
}
generateConstructor(clb);
if (factoryType.parameterCount() == 0 && disableEagerInitialization) {
generateClassInitializer(clb);
}
// 实现函数式接口声明的抽象方法
clb.withMethodBody(interfaceMethodName,
methodDesc(interfaceMethodType),
ACC_PUBLIC,
forwardingMethod(interfaceMethodType));
// 如有必要,生成额外的桥接方法,用于协变返回值类型和类型擦除
if (altMethods != null) {
for (MethodType mt : altMethods) {
clb.withMethodBody(interfaceMethodName,
methodDesc(mt),
ACC_PUBLIC | ACC_BRIDGE,
forwardingMethod(mt));
}
}
// 如果实现了Serializable接口,则额外生成writeReplace方法
if (isSerializable)
generateSerializationFriendlyMethods(clb);
else if (finalAccidentallySerializable)
generateSerializationHostileMethods(clb);
}
});
// 生成字节码后,在JVM中定义该实现类
try {
// this class is linked at the indy callsite; so define a hidden nestmate
var classdata = useImplMethodHandle? implementation : null;
// 这里有一个lambdaProxyClassFileDumper,支持通过VM参数开启,将Lambda实现类的字节码dump出来
return caller.makeHiddenClassDefiner(lambdaClassName, classBytes, lambdaProxyClassFileDumper, NESTMATE_CLASS | STRONG_LOADER_LINK)
.defineClass(!disableEagerInitialization, classdata);
} catch (Throwable t) {
throw new InternalError(t);
}
}

可以看到,Lambda实现类的生成还是比较复杂的,ClassFile API也是较为底层的API,不过在JVM中定义实现类时,传入了一个lambdaProxyClassFileDumper,其定义如下:

// To dump the lambda proxy classes, set this system property:
// -Djdk.invoke.LambdaMetafactory.dumpProxyClassFiles
// or -Djdk.invoke.LambdaMetafactory.dumpProxyClassFiles=true
final String dumpProxyClassesKey = "jdk.invoke.LambdaMetafactory.dumpProxyClassFiles";
lambdaProxyClassFileDumper = ClassFileDumper.getInstance(dumpProxyClassesKey, "DUMP_LAMBDA_PROXY_CLASS_FILES");

注释也提到,支持通过添加-Djdk.invoke.LambdaMetafactory.dumpProxyClassFiles这一VM参数,来转储Lambda实现类的字节码。我们添加这一参数,然后重新运行程序,在项目根目录下的DUMP_LAMBDA_PROXY_CLASS_FILES文件夹中找到了以下类,通过IDEA反编译后的结果如下:

例子1
// 例子1生成的Lambda实现类:LambdaTest1$$Lambda.0x0000021238003548.class
// $FF: synthetic class
final class LambdaTest1$$Lambda implements Runnable {
private LambdaTest1$$Lambda() {
}
public void run() {
LambdaTest1.lambda$main$0();
}
}
例子2
// 例子2生成的Lambda实现类:LambdaTest1$$Lambda.0x0000021238003778.class
// $FF: synthetic class
final class LambdaTest1$$Lambda implements Runnable {
private LambdaTest1$$Lambda() {
}
public void run() {
LambdaTest1.printMessage();
}
}

可以看到,程序在运行期动态生成了两个随机名称的内部类,分别对应我们代码中的两个例子。至此,我们大致了解了Lambda在底层是怎么实现的。

登堂入室,有状态Lambda以及方法签名适配

接下来,我们再来看一些更复杂的场景:

public class LambdaTest2 {
private static int staticValue = 0;
private int instanceValue = 100;
public static void main() {
// 例子3: 捕获静态变量的Lambda
IntUnaryOperator addValue = (x) -> x + staticValue;
System.out.println("Result 3: " + addValue.applyAsInt(1)); //1
staticValue = 1;
System.out.println("Result 3: " + addValue.applyAsInt(1)); // 2
// 例子4: 捕获实例变量的Lambda
LambdaTest2 obj = new LambdaTest2();
IntUnaryOperator addInstanceValue = (x) -> x + obj.instanceValue;
System.out.println("Result 4: " + addInstanceValue.applyAsInt(1)); // 101
obj.instanceValue = 101;
System.out.println("Result 4: " + addInstanceValue.applyAsInt(1)); // 102
// 例子5: 捕获局部变量的Lambda
int localValue = 5;
IntUnaryOperator addLocalValue = (x) -> x + localValue;
System.out.println("Result 5: " + addLocalValue.applyAsInt(1)); // 6
// localValue = 6; // 由于Java的限制,被Lambda表达式捕获的局部变量必须是final或effectively final
// 例子6: 实例方法引用
obj.instanceValue = 99;
IntUnaryOperator addInstanceMethod = obj::addToInstanceValue;
System.out.println("Result 6: " + addInstanceMethod.applyAsInt(1)); // 100
obj.instanceValue = 100;
System.out.println("Result 6: " + addInstanceMethod.applyAsInt(1)); // 101
// 例子7: 参数与返回值类型适配
Consumer<Integer> anotherAdd = LambdaTest2::addToStaticValue;
anotherAdd.accept(1);
System.out.println("Result 7: " + staticValue); // 2
}
private static int addToStaticValue(int x) {
return staticValue += x;
}
private int addToInstanceValue(int x) {
return instanceValue += x;
}
}

通过反编译,我们看到编译器在LambdaTest2这个类中额外生成了以下几个私有静态方法:

private static int lambda$main$0(int var0) {
return var0 + LambdaTest2.staticValue;
}
private static int lambda$main$1(LambdaTest2 var0, int var1) {
return var1 + var0.instanceValue;
}
private static int lambda$main$2(int var0, int var1) {
return var0 + var1;
}

接下来,添加-Djdk.invoke.LambdaMetafactory.dumpProxyClassFiles参数,运行一次程序,我们从DUMP_LAMBDA_PROXY_CLASS_FILES文件夹中找到了以下类:

例子3
// $FF: synthetic class
final class LambdaTest2$$Lambda implements IntUnaryOperator {
private LambdaTest2$$Lambda() {
}
public int applyAsInt(int var1) {
return LambdaTest2.lambda$main$0(var1);
}
}
例子4
// $FF: synthetic class
final class LambdaTest2$$Lambda implements IntUnaryOperator {
private final LambdaTest2 arg$1;
// 通过构造方法传入LambdaTest2实例,以便下面获取该类的实例变量
private LambdaTest2$$Lambda(LambdaTest2 var1) {
this.arg$1 = var1;
}
public int applyAsInt(int var1) {
return LambdaTest2.lambda$main$1(this.arg$1, var1);
}
}
例子5
// $FF: synthetic class
final class LambdaTest2$$Lambda implements IntUnaryOperator {
private final int arg$1;
// 通过构造方法传入局部变量
private LambdaTest2$$Lambda(int var1) {
this.arg$1 = var1;
}
public int applyAsInt(int var1) {
return LambdaTest2.lambda$main$2(this.arg$1, var1);
}
}
例子6
// $FF: synthetic class
final class LambdaTest2$$Lambda implements IntUnaryOperator {
private final LambdaTest2 arg$1;
private LambdaTest2$$Lambda(LambdaTest2 var1) {
this.arg$1 = var1;
}
public int applyAsInt(int var1) {
return this.arg$1.addToInstanceValue(var1);
}
}
例子7
// $FF: synthetic class
final class LambdaTest2$$Lambda implements Consumer {
private LambdaTest2$$Lambda() {
}
public void accept(Object var1) {
LambdaTest2.addToStaticValue(((Integer)var1).intValue()); // 自动进行装箱拆箱
// 忽略了addToStaticValue的返回值,以适配Consumer接口
}
}

所以,最终等价于以下代码:

public class LambdaTest2 {
private static int staticValue = 0;
private int instanceValue = 100;
public static void main() {
// 例子3: 捕获静态变量的Lambda
IntUnaryOperator addValue = Lambda_0.LAMBDA_INSTANCE$;
System.out.println("Result 3: " + addValue.applyAsInt(1));
staticValue = 1;
System.out.println("Result 3: " + addValue.applyAsInt(1));
// 例子4: 捕获实例变量的Lambda
LambdaTest2 obj = new LambdaTest2();
IntUnaryOperator addInstanceValue = new Lambda_1(obj);
System.out.println("Result 4: " + addInstanceValue.applyAsInt(1));
obj.instanceValue = 101;
System.out.println("Result 4: " + addInstanceValue.applyAsInt(1));
// 例子5: 捕获局部变量的Lambda
int localValue = 5;
IntUnaryOperator addLocalValue = new Lambda_2(localValue);
System.out.println("Result 5: " + addLocalValue.applyAsInt(1));
// localValue = 6; // 由于Java的限制,被Lambda表达式捕获的局部变量必须是final或effectively final
// 例子6: 实例方法引用
IntUnaryOperator addInstanceMethod = new Lambda_3(obj);
System.out.println("Result 6: " + addInstanceMethod.applyAsInt(1));
obj.instanceValue = 100;
System.out.println("Result 6: " + addInstanceMethod.applyAsInt(1));
// 例子7: 参数与返回值类型适配
Consumer<Integer> anotherAdd = Lambda_4.LAMBDA_INSTANCE$;
anotherAdd.accept(1);
System.out.println("Result 6: " + staticValue);
}
private static int addToStaticValue(int x) {
return staticValue += x;
}
private int addToInstanceValue(int x) {
return instanceValue += x;
}
// 例子3: 捕获静态变量的Lambda
private static int lambda$main$0(int var0) {
return var0 + LambdaTest2.staticValue;
}
final static class Lambda_0 implements IntUnaryOperator {
private static final Lambda_0 LAMBDA_INSTANCE$ = new Lambda_0();
private Lambda_0() {
}
public int applyAsInt(int var1) {
return LambdaTest2.lambda$main$0(var1);
}
}
// 例子4: 捕获实例变量的Lambda
private static int lambda$main$1(LambdaTest2 var0, int var1) {
return var1 + var0.instanceValue;
}
final static class Lambda_1 implements IntUnaryOperator {
private final LambdaTest2 arg$1;
private Lambda_1(LambdaTest2 var1) {
this.arg$1 = var1;
}
public int applyAsInt(int var1) {
return LambdaTest2.lambda$main$1(this.arg$1, var1);
}
}
// 例子5: 捕获局部变量的Lambda
private static int lambda$main$2(int var0, int var1) {
return var0 + var1;
}
final static class Lambda_2 implements IntUnaryOperator {
private final int arg$1;
private Lambda_2(int var1) {
this.arg$1 = var1;
}
public int applyAsInt(int var1) {
return LambdaTest2.lambda$main$2(this.arg$1, var1);
}
}
// 例子6: 实例方法引用
final static class Lambda_3 implements IntUnaryOperator {
private final LambdaTest2 arg$1;
private Lambda_3(LambdaTest2 var1) {
this.arg$1 = var1;
}
public int applyAsInt(int var1) {
return this.arg$1.addToInstanceValue(var1);
}
}
// 例子7: 参数与返回值类型适配
final static class Lambda_4 implements Consumer {
private static final Lambda_4 LAMBDA_INSTANCE$ = new Lambda_4();
private Lambda_4() {
}
public void accept(Object var1) {
LambdaTest2.addToStaticValue(((Integer) var1).intValue());
}
}
}

至此,我们一步步揭开了Lambda的神秘面纱,并且从中了解到Lambda的几个特点:

  • 无捕获或者捕获类静态变量的Lambda会被优化为单例模式,避免每次创建Lambda实例
  • Lambda捕获的局部变量必须是final声明的或者事实上final(没有被重新赋值),笔者猜测是故意设置的这种限制,避免程序员使用不当造成困扰。
  • Lambda会自动对参数及返回值进行装箱拆箱,并且无返回值的接口方法可以自动适配带返回值的方法实现。

SerializedLambda——安全地实现Lambda序列化

前面我们分析InnerClassLambdaMetafactory#generateInnerClass方法生成Lambda实现类时,发现了如果函数式接口实现了Serializable接口,则通过generateSerializationFriendlyMethods额外生成序列化友好的方法。由此可见,Lambda表达式也是支持序列化的。但是Lambda表达式说到底只是一个承载了一些代码逻辑的方法,JDK是如何实现把代码逻辑序列化到字节数组中,同时还能保证安全的呢?不妨通过一个例子继续分析:

public class LambdaTest3 {
interface SFunction<T, R> extends Function<T, R>, Serializable {
}
public static void main() {
// 例子8:方法引用
SFunction<String, Integer> sFunc = String::length;
System.out.println(sFunc.apply("hello")); // 5
// 例子9:Lambda体
SFunction<String, Integer> sFunc2 = str -> -str.length();
System.out.println(sFunc2.apply("hello")); // -5
// 将例子8的Lambda序列化
var baos = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(baos);
objectOutputStream.writeObject(sFunc);
objectOutputStream.close();
// 再反序列化回来
byte[] serializedLambda = baos.toByteArray();
ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(serializedLambda));
SFunction<String, Integer> deserializedFunc = (SFunction<String, Integer>) objectInputStream.readObject();
objectInputStream.close();
System.out.println(deserializedFunc.apply("hello")); // 5
}
}

同样添加-Djdk.invoke.LambdaMetafactory.dumpProxyClassFiles参数,运行一次程序,我们看看生成的Lambda实现类:

例子8
import java.lang.invoke.SerializedLambda;
// $FF: synthetic class
final class LambdaTest3$$Lambda implements LambdaTest3.SFunction {
private LambdaTest3$$Lambda() {
}
public Object apply(Object var1) {
return ((String)var1).length();
}
private final Object writeReplace() {
return new SerializedLambda(LambdaTest3.class,
"study/invokedynamic/LambdaTest3$SFunction",
"apply",
"(Ljava/lang/Object;)Ljava/lang/Object;",
5,
"java/lang/String",
"length",
"()I",
"(Ljava/lang/String;)Ljava/lang/Integer;",
new Object[0]);
}
}
例子9
import java.lang.invoke.SerializedLambda;
// $FF: synthetic class
final class LambdaTest3$$Lambda implements LambdaTest3.SFunction {
private LambdaTest3$$Lambda() {
}
public Object apply(Object var1) {
return LambdaTest3.lambda$main$699ec26b$1((String)var1);
}
private final Object writeReplace() {
return new SerializedLambda(LambdaTest3.class,
"study/invokedynamic/LambdaTest3$SFunction",
"apply",
"(Ljava/lang/Object;)Ljava/lang/Object;",
6,
"study/invokedynamic/LambdaTest3",
"lambda$main$699ec26b$1",
"(Ljava/lang/String;)Ljava/lang/Integer;",
"(Ljava/lang/String;)Ljava/lang/Integer;",
new Object[0]);
}
}

可以看到,Lambda实现类为序列化额外生成了writeReplace方法。根据Java序列化规范,如果一个类定义了Object writeReplace()方法,那么在序列化该类的实例时,实际被序列化的是writeReplace方法的返回值。

所以Lambda实例在序列化时会被该方法返回的SerializedLambda对象代替,而这个对象中储存了可用于重建这个Lambda一些元信息,包括Lambda表达式所在类、实现的函数式接口名称、实现方法所在类,实现方法的名称等。

SerializedLambda类中定义了一个readResolve方法。根据规范,如果一个类定义了Object readResolve() throws ObjectStreamException方法,那么在ObjectInputStream从输入流中读取并创建了一个该类的实例之后,在返回给调用者之前,JVM会自动检查并调用这个readResolve方法。最终返回给调用者的,将是readResolve方法的返回值,而不是最初创建的那个对象。

SerializedLambdareadResolve方法逻辑如下:

private Object readResolve() throws ObjectStreamException {
try {
Method deserialize = capturingClass.getDeclaredMethod("$deserializeLambda$", SerializedLambda.class);
deserialize.setAccessible(true);
return deserialize.invoke(null, this);
} catch (ReflectiveOperationException roe) {
throw new InvalidObjectException("ReflectiveOperationException during deserialization", roe);
}
}

再看看编译器为LambdaTest3这个类生成了哪些额外的方法:

private static Integer lambda$main$699ec26b$1(String str) {
return -str.length();
}
private static Object $deserializeLambda$(SerializedLambda lambda) {
switch (lambda.getImplMethodName()) {
case "length": {
if (lambda.getImplMethodKind() != 5 ||
!lambda.getFunctionalInterfaceClass().equals("study/invokedynamic/LambdaTest3$SFunction") ||
!lambda.getFunctionalInterfaceMethodName().equals("apply") ||
!lambda.getFunctionalInterfaceMethodSignature().equals("(Ljava/lang/Object;)Ljava/lang/Object;") ||
!lambda.getImplClass().equals("java/lang/String") ||
!lambda.getImplMethodSignature().equals("()I")
) break;
return (SFunction<String, Integer>) String::length;
}
case "lambda$main$699ec26b$1": {
if (lambda.getImplMethodKind() != 6 ||
!lambda.getFunctionalInterfaceClass().equals("study/invokedynamic/LambdaTest3$SFunction") ||
!lambda.getFunctionalInterfaceMethodName().equals("apply") ||
!lambda.getFunctionalInterfaceMethodSignature().equals("(Ljava/lang/Object;)Ljava/lang/Object;") ||
!lambda.getImplClass().equals("study/invokedynamic/LambdaTest3") ||
!lambda.getImplMethodSignature().equals("(Ljava/lang/String;)Ljava/lang/Integer;")
) break;
return (SFunction<String, Integer>) str -> -str.length();
}
}
throw new java.lang.IllegalArgumentException("Invalid lambda deserialization");
}

这下就能对应得上了,Lambda的序列化/反序列化流程可以归纳为:

编译期

为Lambda所在类生成

$deserializeLambda$方法

运行期

生成Lambda实现类时添加

writeReplace方法

序列化

序列化

调用writeReplace

序列化

Lambda实例

委托到实现类的

writeReplace方法

返回SerializedLambda对象

其中包含了能够重建

Lambda的一系列元信息

将SerializedLambda对象

序列化成字节数组

反序列化

反序列化

触发调用readResolve

调用$deserializeLambda$

字节数组

SerializedLambda对象

反射获取Lambda所在类的

$deserializeLambda$方法

检查信息是否匹配,

并重建Lambda实例

实际应用场景

那么,深入了解了Lambda表达式的底层实现后,我们可以在哪些场景下利用这些特性呢?

小试牛刀,实现逼近原生性能的动态Getter/Setter

LambdaMetafactory 除了用于实现Lambda表达式之外,本身也为我们提供了一个强大的武器:在运行时动态地调用任意对象中声明的方法(受JVM权限校验限制),并且其性能可以逼近原生代码调用。

在一些需要动态操作对象属性的场景中,我们通常会想到使用Java的反射API。然而,反射的性能开销是众所周知的,因为它涉及权限检查、方法解析等一系列耗时操作;虽然方法句柄的出现提供了一种比反射更高效的选择,但想要让方法句柄的调用被JIT优化为原生调用,最关键的前提是方法句柄实例需要被声明为 static final。这使得它在需要为非预知的字段创建访问器的场景下显得力不从心。

LambdaMetafactory 恰好能完美地解决这个矛盾。我们可以在运行时,为某个类的特定字段的Getter/Setter方法,手动调用 LambdaMetafactory.metafactory 来动态生成一个实现了标准函数式接口(如 Getter对应Function、Setter对应BiConsumer)的Lambda实例。由于Lambda基于内部类实现的特点,调用这个Lambda实例,其性能几乎与直接编写的 object.getField()object.setField(...) 代码无异。

当然,口说无凭,我们跑上几轮Benchmark来证明。以下是Benchmark代码:

点击展开Benchmark代码
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.lang.invoke.*;
import java.lang.reflect.Method;
import java.util.function.BiConsumer;
import java.util.function.Function;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
/**
* @author brooke_zb
*/
@SuppressWarnings("unchecked")
@State(Scope.Thread)
@BenchmarkMode(Mode.Throughput)
@Warmup(iterations = 100, time = 100, timeUnit = MILLISECONDS)
@Measurement(iterations = 100, time = 100, timeUnit = MILLISECONDS)
@Threads(8)
@OutputTimeUnit(MILLISECONDS)
public class LambdaBenchmark {
public static class Point {
private Double x;
private Double y;
public Point(Double x, Double y) {
this.x = x;
this.y = y;
}
public Double getX() {
return x;
}
public void setX(Double x) {
this.x = x;
}
public Double getY() {
return y;
}
public void setY(Double y) {
this.y = y;
}
}
private static final Function<Point, Double> X_GETTER;
private static final Function<Point, Double> Y_GETTER;
private static final BiConsumer<Point, Double> X_SETTER;
private static final BiConsumer<Point, Double> Y_SETTER;
private static final MethodHandle X_GETTER_MH;
private static final MethodHandle Y_GETTER_MH;
private static final MethodHandle X_SETTER_MH;
private static final MethodHandle Y_SETTER_MH;
private static final Method X_GETTER_METHOD;
private static final Method Y_GETTER_METHOD;
private static final Method X_SETTER_METHOD;
private static final Method Y_SETTER_METHOD;
static {
try {
var lookup = MethodHandles.lookup();
X_GETTER_MH = lookup.findVirtual(Point.class, "getX", MethodType.methodType(Double.class));
Y_GETTER_MH = lookup.findVirtual(Point.class, "getY", MethodType.methodType(Double.class));
X_SETTER_MH = lookup.findVirtual(Point.class, "setX", MethodType.methodType(void.class, Double.class));
Y_SETTER_MH = lookup.findVirtual(Point.class, "setY", MethodType.methodType(void.class, Double.class));
X_GETTER = (Function<Point, Double>) LambdaMetafactory.metafactory(
lookup,
"apply",
MethodType.methodType(Function.class),
MethodType.methodType(Object.class, Object.class),
X_GETTER_MH,
MethodType.methodType(Double.class, Point.class)
).getTarget().invokeExact();
Y_GETTER = (Function<Point, Double>) LambdaMetafactory.metafactory(
lookup,
"apply",
MethodType.methodType(Function.class),
MethodType.methodType(Object.class, Object.class),
Y_GETTER_MH,
MethodType.methodType(Double.class, Point.class)
).getTarget().invokeExact();
X_SETTER = (BiConsumer<Point, Double>) LambdaMetafactory.metafactory(
lookup,
"accept",
MethodType.methodType(BiConsumer.class),
MethodType.methodType(void.class, Object.class, Object.class),
X_SETTER_MH,
MethodType.methodType(void.class, Point.class, Double.class)
).getTarget().invokeExact();
Y_SETTER = (BiConsumer<Point, Double>) LambdaMetafactory.metafactory(
lookup,
"accept",
MethodType.methodType(BiConsumer.class),
MethodType.methodType(void.class, Object.class, Object.class),
Y_SETTER_MH,
MethodType.methodType(void.class, Point.class, Double.class)
).getTarget().invokeExact();
X_GETTER_METHOD = Point.class.getMethod("getX");
Y_GETTER_METHOD = Point.class.getMethod("getY");
X_SETTER_METHOD = Point.class.getMethod("setX", Double.class);
Y_SETTER_METHOD = Point.class.getMethod("setY", Double.class);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
private final Point point = new Point(1.1, 2.2);
// 直接调用,作为基准性能
@Benchmark
public Point plain() {
Double x = point.getX();
Double y = point.getY();
point.setX(y);
point.setY(x);
return point;
}
// static final声明的方法句柄
@Benchmark
public Point methodHandle() throws Throwable {
Double x = (Double) X_GETTER_MH.invokeExact(point);
Double y = (Double) Y_GETTER_MH.invokeExact(point);
X_SETTER_MH.invokeExact(point, y);
Y_SETTER_MH.invokeExact(point, x);
return point;
}
// 使用LambdaMetafactory生成的lambda实例
@Benchmark
public Point lambda() {
Double x = X_GETTER.apply(point);
Double y = Y_GETTER.apply(point);
X_SETTER.accept(point, y);
Y_SETTER.accept(point, x);
return point;
}
// 反射调用
@Benchmark
public Point reflection() throws Throwable {
Double x = (Double) X_GETTER_METHOD.invoke(point);
Double y = (Double) Y_GETTER_METHOD.invoke(point);
X_SETTER_METHOD.invoke(point, y);
Y_SETTER_METHOD.invoke(point, x);
return point;
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(LambdaBenchmark.class.getSimpleName())
.output("./lambda_benchmark.log")
.forks(2)
.build();
new Runner(opt).run();
}

在笔者的Win11 24H2,i7-14700F下跑出的结果

Benchmark Mode Cnt Score Error Units
LambdaBenchmark.lambda thrpt 200 6880990.881 ± 88188.596 ops/ms
LambdaBenchmark.methodHandle thrpt 200 6549743.700 ± 23655.703 ops/ms
LambdaBenchmark.plain thrpt 200 6527813.453 ± 23802.944 ops/ms
LambdaBenchmark.reflection thrpt 200 2009981.376 ± 8497.101 ops/ms

这下给我整不会了,lambda的性能怎么比直接调用还高?跑了好几次都是差不多的结果,lambda、方法句柄、直接调用的性能相差不超过10%。

感谢梦佬提供的数据,在linux x86服务器的测试结果如下:

梦佬提供的linux服务器下测试结果

可以看到,三者的性能相差不超过1%,可以说明lambda的效率和直接调用无异!

登堂入室,通过SerializedLambda获取引用方法的信息

在MyBatis-Plus这样的现代ORM框架中,我们经常看到如下用法:

LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getName, "John");

通过User::getName这个方法引用来指定数据库列,相比于硬编码"name"字符串,这种方式更加类型安全,当字段名name发生重构时,编译器会立刻报错。但是wrapper#eq方法接收到的只是一个函数式接口的实例,并不能获取到方法引用的信息。

我们深入源码会发现,LambdaQueryWrapper中接受方法引用的函数式接口(如SFunction)都继承了Serializable接口。因此,MyBatis-Plus正是通过Lambda序列化的特性,获取并调用我们传入的Lambda实例中的writeReplace方法,获取SerializedLambda对象,进而调用getImplMethodName()来拿到字符串"getName",再根据JavaBean规范推导出数据库列名"name"。从而解决了硬编码列名所造成的问题。

加深理解,invokedynamic魅力不止于此

通过indy指令,Java的Lambda表达式实现得既高效又灵活,充分体现了运行时优化的潜力。然而indy的魅力远不止于此,它已经成为现代Java诸多重要特性的基石:

  • JDK 9的字符串拼接优化: String a = s1 + s2;不再被编译为StringBuilder的调用,而是编译为一条indy指令,通过一系列操作实现比StringBuilder更好的性能
  • JDK 21的switch模式匹配: 复杂的模式匹配规则也是通过indy在底层实现的。

在后续的文章中,我们将继续揭秘这些特性。

到此,有的读者可能会认为,indy指令无法被普通开发者利用,更多是被一些建立于JVM之上的动态语言利用,比如Kotlin、Groovy。诚然,如果不利用ClassFile或者ASM这类字节码工具库,我们无法直接在代码中使用indy指令;如果你对字节码足够熟悉,利用这些工具库,普通开发者也可以间接利用indy的威力。例如梦佬就利用indy指令实现了一种比传统DCL模式性能更好的懒汉式单例:dreamlike-ocean/StableValue: 使用invokedynamic实现的零依赖StableValue用于取代DCL模式

不过,我们可以利用一些小技巧来间接让一个indy指令指向我们自定义的BSM。下面将用一个例子来加深读者对indy指令的理解,用来结束这一篇文章我认为再好不过:

import java.lang.invoke.*;
import java.util.function.Function;
/**
* @author brooke_zb
*/
public class CustomLambdaBootstrap {
static void main() {
printSomething("hello");
printSomething("custom");
printSomething("lambda");
}
private static void printSomething(String s) {
Function<String, Integer> f = String::length;
System.out.println(f.apply(s));
}
/**
* BSM只规范了参数和返回值,只要遵循规范,我们也可以自定义一个BSM
* 只不过现在我们无法直接让invokedynamic指令调用这个方法
* 所以我们将{@link java.lang.invoke.LambdaMetafactory#metafactory}方法的参数照搬一遍
* 准备来一个偷天换日
*/
private static CallSite myBootstrap(MethodHandles.Lookup caller,
String interfaceMethodName,
MethodType factoryType,
MethodType interfaceMethodType,
MethodHandle implementation,
MethodType dynamicMethodType) {
System.out.printf("[Custom BSM] caller: %s\n", caller);
System.out.printf("[Custom BSM] interfaceMethodName: %s\n", interfaceMethodName);
System.out.printf("[Custom BSM] factoryType: %s\n", factoryType);
Function<String, Integer> f;
if (Math.random() > 0.5) {
f = new Function<>() {
// 匿名内部类构造函数
{
System.out.println("[Custom Function] choose impl 1!");
}
@Override
public Integer apply(String s) {
System.out.printf("apply string is: %s\n", s);
return s.hashCode();
}
};
} else {
f = new Function<>() {
{
System.out.println("[Custom Function] choose impl 2!");
}
@Override
public Integer apply(String s) {
System.out.printf("reversed string is: %s\n", new StringBuilder(s).reverse());
return s.indexOf("o");
}
};
}
MethodHandle fMh = MethodHandles.constant(Function.class, f);
return new ConstantCallSite(fMh);
}
}

以上代码编译后运行,如我们预想一样输出了三个字符串的长度:

5
6
6

然后,为了更方便地修改这个类的字节码,我们需要用到IDEA的一个插件:jclasslib。安装后通过菜单打开刚刚这个类的字节码:

jclasslib插件展示字节码

然后定位到BootstrapMethods区,点击序号0的Bootstrap方法中的链接跳转到常量表

定位到BootstrapMethods

跳转到常量表

然后编辑类名为我们这个类的路径,假设这个类位于study.invokedynamic包下,那么我们将类名设置为study/invokedynamic/CustomLambdaBootstrap。接着将方法名设置为我们编写的自定义BSM方法:myBootstrap

然后点击左上角的保存图标,选择Overwrite。完成后,多跑几次程序,我们便能得到以下两种结果:

[Custom BSM] caller: study.invokedynamic.CustomLambdaBootstrap
[Custom BSM] interfaceMethodName: apply
[Custom BSM] factoryType: ()Function
[Custom Function] choose impl 1!
apply string is: hello
99162322
apply string is: custom
-1349088399
apply string is: lambda
-1110092857
[Custom BSM] caller: study.invokedynamic.CustomLambdaBootstrap
[Custom BSM] interfaceMethodName: apply
[Custom BSM] factoryType: ()Function
[Custom Function] choose impl 2!
reversed string is: olleh
4
reversed string is: motsuc
4
reversed string is: adbmal
-1

通过这个例子,相信能让读者们更深刻地理解indy指令的特性:

  • 运行期第一次执行时调用BSM,动态绑定调用点
  • 后续执行无需引导,直接调用已绑定的调用点,性能更高
  • 更加灵活,支持各种动态场景,而非编译期写死