08 匿名内部类与 lambda —— lambda 表达式背后的字节码原理
问:lambda 表达式是匿名内部类的语法糖吗
答案在末尾
一、测试匿名内部类
public class Test {
public static void main(String[] args) {
Runnable r1 = new Runnable() {
@Override
public void run() {
System.out.println("hello, inner class");
}
};
r1.run();
}
}
javap 查看字节码
javap -c -v Test
复制 main() 函数部分,如下
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #16 // class Test$1
3: dup
4: invokespecial #18 // Method Test$1."<init>":()V
7: astore_1
8: aload_1
9: invokeinterface #19, 1 // InterfaceMethod java/lang/Runnable.run:()V
14: return
- 6 ~ 8 行:新建 Test$1 实例对象
- 9 行:astore_1 指令把对象引用放到局部变量表下标为 1 的位置
- 10 行:aload_1 指令将对象引用从局部变量表下标为 1 的位置放到操作数栈栈顶
- 11 行:执行 Test$1 对象的 run 方法
这里的 Test$1 实例对象就是代码中的 r1
二、测试 lambda 表达式
还是上面的代码,改成 lambda 的形式
public class Test {
public static void main(String[] args) {
Runnable r1 = () -> {
System.out.println("hello, lambda");
};
r1.run();
}
}
javap 查看字节码
javap -c -v Test
复制 main() 函数和 lambda 相关的部分,如下
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 #16, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
5: astore_1
6: aload_1
7: invokeinterface #20, 1 // InterfaceMethod java/lang/Runnable.run:()V
12: return
private static void lambda$0();
descriptor: ()V
flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #29 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #35 // String hello, lambda
5: invokevirtual #37 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
- 12 ~ 20 行:这是一个名为 lambda$0 的静态方法,方法内容很简单,打印输出字符串 hello, lambda
- 6 行:invokedynamic 指令很神奇,#16 表示常量池的 #16
下面看下常量池
Constant pool:
...
#16 = InvokeDynamic #0:#17 // #0:run:()Ljava/lang/Runnable;
#17 = NameAndType #18:#19 // run:()Ljava/lang/Runnable;
#18 = Utf8 run
#19 = Utf8 ()Ljava/lang/Runnable;
...
常量池的 #16 所指向的内容中,#0 对应 BootstrapMethods 中的第 0 行
核心的 metafactory 定义
public static CallSite metafactory(
MethodHandles.Lookup caller,
String invokedName,
MethodType invokeType,
MethodType samMethodType,
MethodHandle implMethod,
MethodType instantiatedMethodType
)
参数
- caller:JVM 提供的查找上下文
- invokedName:表示调用函数名,在本例中
- invokedName 为 “run”
- samMethodType:函数式接口定义的方法签名(参数类型和返回值类型)本例中为 run 方法的签名 “()void”
- implMethod:编译时生成的 lambda 表达式对应的静态方法 invokestatic Test:lambda$main$0
- instantiatedMethodType:一般和 samMethodType 是一样的(或是它的一个特例),在本例中是 “()void”
核心的 metafactory() 函数
public static CallSite metafactory(MethodHandles.Lookup caller,
String invokedName,
MethodType invokedType,
MethodType samMethodType,
MethodHandle implMethod,
MethodType instantiatedMethodType) throws LambdaConversionException {
AbstractValidatingLambdaMetafactory mf;
// 创建了 InnerClassLambdaMetafactory 类实例
mf = new InnerClassLambdaMetafactory(caller, invokedType,
invokedName, samMethodType,
implMethod, instantiatedMethodType,
false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
mf.validateMetafactoryArgs();
return mf.buildCallSite();
}
查看 InnerClassLambdaMetafactory 类
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);
// 关键:看这里,从名字就可以看出这是生成了 lambda 类的名字,生成规则是:类名 + $$Lambda$ + 1(每次获取值时先自动增 1)
lambdaClassName = targetClass.getName().replace('.', '/') + "$$Lambda$" + counter.incrementAndGet();
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;
}
}
打印一下 lambda 的类名
可以看到看到了生成的 lambda 类名为 Test$$Lambda$1/531885035
下面我们打个断点看看,那断点打在哪?
自然是上面说到的 InnerClassLambdaMetafactory 的构造方法中生成 lambdaClassName 的这行代码,代码执行到此行(下图的 165 行)时,再执行一步(下图的 166 行),然后鼠标停留在 lambdaClassName 上方,结果显示 Test$$Lambda$1
控制台打印的是 Test$$Lambda$1/531885035
,断点打印的是 Test$$Lambda$1
,肯定在断点之后,类名加上了 /531885035
再次贴出 metafactory() 函数
public static CallSite metafactory(MethodHandles.Lookup caller,
String invokedName,
MethodType invokedType,
MethodType samMethodType,
MethodHandle implMethod,
MethodType instantiatedMethodType) throws LambdaConversionException {
AbstractValidatingLambdaMetafactory mf;
// 创建了 InnerClassLambdaMetafactory 类实例
mf = new InnerClassLambdaMetafactory(caller, invokedType,
invokedName, samMethodType,
implMethod, instantiatedMethodType,
false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
mf.validateMetafactoryArgs();
// 这里对上面的 InnerClassLambdaMetafactory 实例调用了 buildCallSite() 方法
// 因此生成的 Lambda 类名中斜杆及之后的数字,应该在此方法中生成
return mf.buildCallSite();
}
查看 InnerClassLambdaMetafactory#buildCallSite() 方法
CallSite buildCallSite() throws LambdaConversionException {
// 看下第一行
final Class<?> innerClass = spinInnerClass();
...
}
看下 InnerClassLambdaMetafactory#spinInnerClass() 方法
private Class<?> spinInnerClass() throws LambdaConversionException {
String[] interfaces;
String samIntf = samBase.getName().replace('.', '/');
boolean accidentallySerializable = !isSerializable && Serializable.class.isAssignableFrom(samBase);
if (markerInterfaces.length == 0) {
interfaces = new String[]{samIntf};
} else {
// Assure no duplicate interfaces (ClassFormatError)
// 确保没有重复的接口
Set<String> itfs = new LinkedHashSet<>(markerInterfaces.length + 1);
itfs.add(samIntf);
for (Class<?> markerInterface : markerInterfaces) {
itfs.add(markerInterface.getName().replace('.', '/'));
accidentallySerializable |= !isSerializable && Serializable.class.isAssignableFrom(markerInterface);
}
interfaces = itfs.toArray(new String[itfs.size()]);
}
cw.visit(CLASSFILE_VERSION, ACC_SUPER + ACC_FINAL + ACC_SYNTHETIC,
lambdaClassName, null,
JAVA_LANG_OBJECT, interfaces);
...
// Define the generated class in this VM.
final byte[] classBytes = cw.toByteArray();
// If requested, dump out to a file for debugging purposes
// 如果被请求,为了调试程序,转储时生成一个文件
if (dumper != null) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
@Override
public Void run() {
dumper.dumpClass(lambdaClassName, classBytes);
return null;
}
}, null,
new FilePermission("<<ALL FILES>>", "read, write"),
// createDirectories may need it
new PropertyPermission("user.dir", "read"));
}
return UNSAFE.defineAnonymousClass(targetClass, classBytes, null);
}
1 ~ 21 行,大致就是为了生成不重复的接口,所以在后面加上了 /531885035
说了半天,到底生成的这个 lambda 的类在哪能看到
仔细看看上面的 InnerClassLambdaMetafactory#spinInnerClass() 方法,其中 31 ~42 行可以用来控制是否将生成的类 dump 到磁盘中
那如何进入这个 if 语句,去看看 dumper 变量
// For dumping generated classes to disk, for debugging purposes
// 为了调试程序,将转储的类文件生成到磁盘
private static final ProxyClassesDumper dumper;
static {
final String key = "jdk.internal.lambda.dumpProxyClasses";
String path = AccessController.doPrivileged(
new GetPropertyAction(key), null,
new PropertyPermission(key , "read"));
dumper = (null == path) ? null : ProxyClassesDumper.getInstance(path);
}
到这里,肯定是在执行 java 命令时传递一些什么参数,就能使 dumper 值为 true
java 命令生成 dump 类文件
// 注意:. Test 之间有个空格
// . 表示当前目录
// 将 dump 的类文件生成到在当前目录
java -Djdk.internal.lambda.dumpProxyClasses=. Test
可以看到生成了一个内部类:Test$$Lambda$1.class,这个类是由 InnerClassLambdaMetafactory 使用 ASM 字节码技术动态生成的,默认情况下看不到而已
我们 javap 一下这个内部类 Test$$Lambda$1.class 吧
javap -c -v Test$$Lambda$1
全部内容,如下
final class Test$$Lambda$1 implements java.lang.Runnable
minor version: 0
major version: 52
flags: ACC_FINAL, ACC_SUPER, ACC_SYNTHETIC
Constant pool:
#1 = Utf8 Test$$Lambda$1
#2 = Class #1 // Test$$Lambda$1
#3 = Utf8 java/lang/Object
#4 = Class #3 // java/lang/Object
#5 = Utf8 java/lang/Runnable
#6 = Class #5 // java/lang/Runnable
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = NameAndType #7:#8 // "<init>":()V
#10 = Methodref #4.#9 // java/lang/Object."<init>":()V
#11 = Utf8 run
#12 = Utf8 Ljava/lang/invoke/LambdaForm$Hidden;
#13 = Utf8 Test
#14 = Class #13 // Test
#15 = Utf8 lambda$0
#16 = NameAndType #15:#8 // lambda$0:()V
#17 = Methodref #14.#16 // Test.lambda$0:()V
#18 = Utf8 Code
#19 = Utf8 RuntimeVisibleAnnotations
{
public void run();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=0, locals=1, args_size=1
0: invokestatic #17 // Method Test.lambda$0:()V
3: return
RuntimeVisibleAnnotations:
0: #12()
}
- 1 行:这个类实现了 Runnable 接口
- 31 行:在 run() 方法 中,invokestatic 指令调用了 Method Test.lambda$0:()V,即 Test 的静态方法 lambda$0()
至此,简述一下整个过程
- lambda 表达式声明的地方会生成一个 invokedynamic 指令,同时编译器会生成一个对应的引导方法的统计(原文说是生成一个对应的引导方法 Bootstrap Method,根据 javap 打印的结果,应该是一个 BootstrapMethods,如果这里直接翻译成引导方法,会误以为是一个起引导作用的方法,而实际生成的信息看上去是一个和常量池一样形式的内容,只是记录了一些方法信息而已),然后会在第一次执行 invokedynamic 指令时调用 LambdaMetafactory#metafactory() 这个引导方法动态生成内部类 Test$$Lambda$1.class,默认这个类不会生成文件,所以看不到
- 引导方法 LambdaMetafactory#metafactory() 会返回一个 CallSite 对象,这个 CallSite 会链接上面生成的 Test$$Lambda$1.class 内部类(实现了 Runnable 接口)
- lambda 表达式中的内容会被编译成静态方法,前面动态生成的内部类会直接调用该静态方法
- 真正执行 lambda 调用的还是用 invokeinterface 指令
画个图
小结
- lambda 表达式在编译期只是生成了一个 invokedynamic 指令,并没有生成匿名内部类
- lambda 表达式中的内容会被编译成一个静态方法
到时候调用这个静态方法,就相当于执行了 lambda 表达式,那么谁来调用这个静态方法呢
既然编译期并没有生成别的什么,只有一个静态方法,那么自然是在运行期做一些小动作了
这个小动作就是,在运行期 LambdaMetafactory.metafactory() 这个工厂方法会根据一些参数生成一个内部类,这个内部类会去调用编译期生成的那个静态方法 - 可以看到,其实整个过程还是会生成一个内部类,只不过 lambda 表达式对应的这个内部类是在运行期生成的
思考题
下面的两段代码分别会生成多少个内部类?为什么?
代码片段 1
for (int i = 0; i < 10; i++) {
Runnable r = () -> {
System.out.println("hello, lambda");
};
r.run();
}
代码片段 2
Runnable r1 = () -> {
System.out.println("hello, lambda");
};
r1.run();
Runnable r2 = () -> {
System.out.println("hello, lambda");
};
r2.run();
回答
思考题答案:代码片段 1 生成一个内部类,代码片段 2 生成两个内部类,因为只有第一次执行 invokedynamic 指令时才会生成对应的内部类
开头问题答案:lambda 表达式不仅仅是想象中的匿名内部类的语法糖那么简单,背后的整个运行逻辑和单纯的匿名内部类是不一样的