前言
最近在浏览博客时偶然了解了Lambda表达式的底层原理,对于这个日常使用的东西,自己还是了解得太浅了,所以记录了一下
结论先行
先忽略中间的细节,看看Lambda表达式是怎么实现的,随便写一段使用了Lambda表达式的代码
public class LambdaTest {
public static void main(String[] args) {
Runnable r = () -> {
System.out.println("lambda");
};
r.run();
}
}
启动前加上下面的配置
-Djdk.internal.lambda.dumpProxyClasses=.
如下文,在当前目录下会生成这样的一个类,run()方法中直接调用LambdaTest的静态方法lambda0(),问题来了,LambdaTest是我们写的类,明明没有lambda0()这个静态方法,是怎么蹦出来的,很容易就想到是编译的时候自动生成的,类都可以自动生成,何况是方法,那么这个方法长什么样子?
final class LambdaTest$$Lambda$1 implements Runnable {
private LambdaTest$$Lambda$1() {
}
@Hidden
public void run() {
LambdaTest.lambda$main$0();
}
}
只能看看字节码了,字节码是照妖镜,所有的妖魔鬼怪在字节码面前都无所遁形,用下面这条指令可以看到类中的所有方法
javap -c -p LambdaTest
public class com.example.aop.lambda.LambdaTest {
// 构造器
public com.example.aop.lambda.LambdaTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
// 我们写的main方法
public static void main(java.lang.String[]);
Code:
0: invokedynamic #2, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
5: astore_1
6: aload_1
7: invokeinterface #3, 1 // InterfaceMethod java/lang/Runnable.run:()V
12: return
// 生成的静态方法
// 有些同学可能不太熟悉字节码指令,但是看旁边的注释,应该隐约可以猜到,这些字节码指令对应的代码是
// System.out.println("lambda");
private static void lambda$main$0();
Code:
0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #5 // String lambda
5: invokevirtual #6 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
我们可以先得出这样的结论:使用Lambda表达式时,Lambda表达式中的内容会被编译成静态方法,并且会动态生成一个类调用这个静态方法
那么这个类和静态方法是怎么生成的,这个类是怎么调用这个静态方法的?
MethodHandle
MethodHandle又称为方法句柄,它的出现使得Java可以像其他语言一样把函数当作参数进行传递,下面用一个例子看看它是怎么使用的
public class MethodHandleDemo {
public void print(String s) {
System.out.println("hello, " + s);
}
public static void main(String[] args) throws Throwable {
// 创建MethodType对象,指定方法的返回值类型和参数类型
MethodType methodType = MethodType.methodType(void.class, String.class);
// MethodHandles.lookup()静态方法返回 MethodHandles.Lookup 对象,这个对象表示查找的上下文
// findVirtual查找方法签名为methodType的方法句柄
MethodHandle methodHandle = MethodHandles.lookup().findVirtual(MethodHandleDemo.class, "print", methodType);
// 进行方法调用
MethodHandleDemo methodHandleDemo = new MethodHandleDemo();
methodHandle.invokeExact(methodHandleDemo, "world");
}
}
MethodHandle使用起来跟反射很像,与反射相比,MethodHandle更加灵活和轻量,方法句柄的权限检查是在句柄的创建阶段完成的,在实际调用过程中,Java虚拟机不会检查方法句柄的权限。如果该句柄被多次调用,将省下重复权限检查的开销
上文所说的新生成的类对静态方法的调用就是使用了MethodHandle
invokedynamic和BootstrapMethods
javap -v LambdaTest
使用这个指令看看上面那个例子详细版本的字节码
public class com.example.aop.lambda.LambdaTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #8.#26 // java/lang/Object."<init>":()V
#2 = InvokeDynamic #0:#31 // #0:run:()Ljava/lang/Runnable;
#3 = InterfaceMethodref #32.#33 // java/lang/Runnable.run:()V
#4 = Fieldref #34.#35 // java/lang/System.out:Ljava/io/PrintStream;
#5 = String #36 // lambda
#6 = Methodref #37.#38 // java/io/PrintStream.println:(Ljava/lang/String;)V
#7 = Class #39 // com/example/aop/lambda/LambdaTest
#8 = Class #40 // java/lang/Object
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 this
#15 = Utf8 Lcom/example/aop/lambda/LambdaTest;
#16 = Utf8 main
#17 = Utf8 ([Ljava/lang/String;)V
#18 = Utf8 args
#19 = Utf8 [Ljava/lang/String;
#20 = Utf8 r
#21 = Utf8 Ljava/lang/Runnable;
#22 = Utf8 MethodParameters
#23 = Utf8 lambda$main$0
#24 = Utf8 SourceFile
#25 = Utf8 LambdaTest.java
#26 = NameAndType #9:#10 // "<init>":()V
#27 = Utf8 BootstrapMethods
#28 = MethodHandle #6:#41 // 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;
#29 = MethodType #10 // ()V
#30 = MethodHandle #6:#42 // invokestatic com/example/aop/lambda/LambdaTest.lambda$main$0:()V
#31 = NameAndType #43:#44 // run:()Ljava/lang/Runnable;
#32 = Class #45 // java/lang/Runnable
#33 = NameAndType #43:#10 // run:()V
#34 = Class #46 // java/lang/System
#35 = NameAndType #47:#48 // out:Ljava/io/PrintStream;
#36 = Utf8 lambda
#37 = Class #49 // java/io/PrintStream
#38 = NameAndType #50:#51 // println:(Ljava/lang/String;)V
#39 = Utf8 com/example/aop/lambda/LambdaTest
#40 = Utf8 java/lang/Object
#41 = Methodref #52.#53 // 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;
#42 = Methodref #7.#54 // com/example/aop/lambda/LambdaTest.lambda$main$0:()V
#43 = Utf8 run
#44 = Utf8 ()Ljava/lang/Runnable;
#45 = Utf8 java/lang/Runnable
#46 = Utf8 java/lang/System
#47 = Utf8 out
#48 = Utf8 Ljava/io/PrintStream;
#49 = Utf8 java/io/PrintStream
#50 = Utf8 println
#51 = Utf8 (Ljava/lang/String;)V
#52 = Class #55 // java/lang/invoke/LambdaMetafactory
#53 = NameAndType #56:#60 // 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;
#54 = NameAndType #23:#10 // lambda$main$0:()V
#55 = Utf8 java/lang/invoke/LambdaMetafactory
#56 = Utf8 metafactory
#57 = Class #62 // java/lang/invoke/MethodHandles$Lookup
#58 = Utf8 Lookup
#59 = Utf8 InnerClasses
#60 = Utf8 (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;
#61 = Class #63 // java/lang/invoke/MethodHandles
#62 = Utf8 java/lang/invoke/MethodHandles$Lookup
#63 = Utf8 java/lang/invoke/MethodHandles
{
public com.example.aop.lambda.LambdaTest();
descriptor: ()V
flags: 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 9: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/example/aop/lambda/LambdaTest;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=1
0: invokedynamic #2, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
5: astore_1
6: aload_1
7: invokeinterface #3, 1 // InterfaceMethod java/lang/Runnable.run:()V
12: return
LineNumberTable:
line 12: 0
line 15: 6
line 16: 12
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 args [Ljava/lang/String;
6 7 1 r Ljava/lang/Runnable;
MethodParameters:
Name Flags
args
}
SourceFile: "LambdaTest.java"
InnerClasses:
public static final #58= #57 of #61; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
BootstrapMethods:
0: #28 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:
#29 ()V
#30 invokestatic com/example/aop/lambda/LambdaTest.lambda$main$0:()V
#29 ()V
只看方法相关的字节码,有一个指令比较特殊——invokedynamic
每一个invokedynamic指令的实例叫做一个动态调用点,动态调用点最开始是未链接状态,表示还未指定该调用点要调用的方法,依靠引导方法来链接到具体的方法,比如上文的invokedynamic会查找常量池中的#2,#2会查找#0,#0是一个特殊的查找,对应BootstrapMethods中的0行,可以看到这是对静态方法java.lang.invoke.LambdaMetafactory#metafactory的调用,它的返回值是java.lang.invoke.CallSite对象,这个对象的getTarget方法返回了目标方法句柄。核心的metafactory方法如下
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();
return mf.buildCallSite();
}
这里的几个入参都挺有意思的,跟字节码中的NameAndType类型常量很像
- caller:表示JVM提供的查找上下文,此处为com.example.aop.lambda.LambdaTest
- invokeName:调用的函数名
- invokedType:期望的方法参数的类型和返回值类型,此处为()Runnable
- samMethodType:函数式接口定义的方法签名(参数类型和返回值类型),此处为 ()void
- implMethod:新生成的类实际调用的方法,此处为com.example.aop.lambda.LambdaTest.lambda0()
- instantiatedMethodType:一般和samMethodType是一样的,但在函数式接口为泛型的情况下,这里是实际的参数类型和返回值类型,比如Consumer接口,入参为泛型,如果实际情况下,入参为String,那么此处就是 (String)void
深入进去InnerClassLambdaMetafactory这个类看看,会发现是使用了ASM框架生成新的类
public InnerClassLambdaMetafactory(MethodHandles.Lookup caller,
MethodType invokedType,
String samMethodName,
MethodType samMethodType,
MethodHandle implMethod,
MethodType instantiatedMethodType,
boolean isSerializable,
Class<?>[] markerInterfaces,
MethodType[] additionalBridges)
throws LambdaConversionException {
super(caller, invokedType, samMethodName, samMethodType,
implMethod, instantiatedMethodType,
isSerializable, markerInterfaces, additionalBridges);
implMethodClassName = implDefiningClass.getName().replace('.', '/');
implMethodName = implInfo.getName();
implMethodDesc = implMethodType.toMethodDescriptorString();
implMethodReturnClass = (implKind == MethodHandleInfo.REF_newInvokeSpecial)
? implDefiningClass
: implMethodType.returnType();
constructorType = invokedType.changeReturnType(Void.TYPE);
// 生成的类名
lambdaClassName = targetClass.getName().replace('.', '/') + "$$Lambda$" + counter.incrementAndGet();
// 熟悉的ClassWriter
cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
int parameterCount = invokedType.parameterCount();
if (parameterCount > 0) {
argNames = new String[parameterCount];
argDescs = new String[parameterCount];
for (int i = 0; i < parameterCount; i++) {
argNames[i] = "arg$" + (i + 1);
argDescs[i] = BytecodeDescriptor.unparse(invokedType.parameterType(i));
}
} else {
argNames = argDescs = EMPTY_STRING_ARRAY;
}
}
public class CallSite {
// CallSite中有一个getTarget方法,可以获取MethodHandle,对真正要执行的方法进行调用
public abstract MethodHandle getTarget();
}
总结
- Lambda表达式声明的地方会生成一个invokedynamic指令,同时编译器生成一个对应的引导方法BootstrapMethods
- BootstrapMethods中的引导方法会调用java.lang.invoke.LambdaMetafactory#metafactory动态生成新的类和一个静态方法,并且会返回一个CallSite,通过CallSite中的getTarget方法,可以获取MethodHandle,进行新的类和静态方法的调用
参考资料
深入理解JVM字节码