深入底层原理—带你看透Lambda表达式的本质
Posted Java-桃子
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入底层原理—带你看透Lambda表达式的本质相关的知识,希望对你有一定的参考价值。
前言
嗨咯,大家好,今天给带大家深入底层原理看透Lambda表达式的本质,如果本文对你有帮助,记得一键三连一下!!
知识点
Lambda的原理
Java 8支持动态语言,看到很酷的Lambda表达式,对一直以静态类型语言自居的Java,让人看到了Java虚拟机可以支持动态语言的目标。
Lambda的案例
import java.util.function.Consumer;
public class Lambda {
public static void main(String[] args) {
Consumer<String> c = s -> System.out.println(s);
c.accept("hello lambda!");
}
}
Lambda表达式
刚看到这个表达式,感觉java的处理方式是属于内部匿名类的方式
public class Lambda {
static {
System.setProperty("jdk.internal.lambda.dumpProxyClasses", ".");
}
public static void main(String[] args) {
Consumer<String> c = new Consumer<String>(){
@Override
public void accept(String s) {
System.out.println(s);
}
};
c.accept("hello lambda");
}
}
编译的结果应该是Lambda.class , Lambda$1.class 猜测在支持动态语言java换汤不换药,在最后编译的时候生成我们常见的方式。但是结果不是这样的,只是产生了一个Lambda.class
反编译吧,来看看真相是什么?
javap -v -p Lambda.class
- -p 参数会显示所有的方法,而不带默认是不会反编译private的方法的。
public Lambda();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #21 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LLambda;
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: invokedynamic #30, 0 // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
5: astore_1
6: aload_1
7: ldc #31 // String hello lambda
9: invokeinterface #33, 2 // InterfaceMethod java/util/function/Consumer.accept:(Ljava/lang/Object;)V
14: return
LineNumberTable:
line 8: 0
line 9: 6
line 10: 14
LocalVariableTable:
Start Length Slot Name Signature
0 15 0 args [Ljava/lang/String;
6 9 1 c Ljava/util/function/Consumer;
LocalVariableTypeTable:
Start Length Slot Name Signature
6 9 1 c Ljava/util/function/Consumer<Ljava/lang/String;>;
private static void lambda$0(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #46 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_0
4: invokevirtual #50 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
7: return
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 8 0 s Ljava/lang/String;
}
SourceFile: "Lambda.java"
BootstrapMethods:
0: #66 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:
#67 (Ljava/lang/Object;)V
#70 invokestatic Lambda.lambda$0:(Ljava/lang/String;)V
#71 (Ljava/lang/String;)V
InnerClasses:
public static final #77= #73 of #75; //Lookup=class java/lang/invoke/MethodHandles$Lookup of class java/lang/invoke/MethodHandles
重点关注方法
Invokedynamic
- Java的调用函数的四大指令(invokevirtual、invokespecial、invokestatic、invokeinterface),通常方法的符号引用在静态类型语言编译时就能产生。
- 动态类型语言只有在运行期才能确定接收者类型,改变四大指令的语意对java的版本有很大的影响,所以在JSR 292 《Supporting Dynamically Typed Languages on the Java Platform》添加了一个新的指令:Invokedynamic。
// InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
0: invokedynamic #30,0
-
#30 是代表常量#30也就是后面的注释InvokeDynamic #0🉑 ()Ljava/util/function/Consumer;
-
0 是占位符号,目前无用
BootstrapMethods
每一个invokedynamic指令的实例叫做一个动态调用点(dynamic call site),动态调用点最开始是未链接状态(unlinked):表示还未指定该调用点要调用的方法), 动态调用点依靠引导方法来链接到具体的方法。
引导方法是由编译器生成,在运行期当JVM第一次遇到invokedynamic指令时, 会调用引导方法来将invokedynamic指令所指定的名字(方法名,方法签名)和具体的执行代码(目标方法)链接起来, 引导方法的返回值永久的决定了调用点的行为。
CallSite
引导方法的返回值类型是java.lang.invoke.CallSite,一个invokedynamic指令关联一个CallSite,将所有的调用委托到CallSite当前的target(MethodHandle)
InvokeDynamic #0 就是BootstrapMethods表示#0的位置
0: #66 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:
#67 (Ljava/lang/Object;)V
#70 invokestatic Lambda.lambda$0:(Ljava/lang/String;)V
#71 (Ljava/lang/String;)V
我们看到调用了LambdaMetaFactory.metafactory的方法
参数:
- LambdaMetafactory.metafactory(Lookup, String, MethodType,
MethodType,MethodHandle, MethodType) 有六个参数, 按顺序描述如下
1.MethodHandles.Lookup caller : 代表查找上下文与调用者的访问权限, 使用invokedynamic指令时,JVM会自动自动填充这个参数。
2.String invokedName : 要实现的方法的名字, 使用invokedynamic时,JVM自动帮我们填充(填充内容来自常量池InvokeDynamic.NameAndType.Name), 在这里JVM为我们填充为 “apply”, 即Consumer.accept方法名。
3.MethodType invokedType : 调用点期望的方法参数的类型和返回值的类型(方法signature)。
- 使用invokedynamic指令时,JVM会自动自动填充这个参数(填充内容来自常量池InvokeDynamic.NameAndType.Type),在这里参数为String, 返回值类型为Consumer, 表示这个调用点的目标方法的参数为String,然后invokedynamic执行完后会返回一个即Consumer实例。
4.MethodType samMethodType : 函数对象将要实现的接口方法类型,这里运行时, 值为 (Object)Object 即 Consumer.accept方法的类型(泛型信息被擦除)。
#67 (Ljava/lang/Object;)V
5.MethodHandle implMethod : 一个直接方法句柄(DirectMethodHandle), 描述在调用时将被执行的具体实现方法 (包含适当的参数适配, 返回类型适配, 和在调用参数前附加上捕获的参数)。
- 在这里为 #70 invokestatic Lambda.lambda$0:(Ljava/lang/String;)V 方法的方法句柄.
6.MethodType instantiatedMethodType : 函数接口方法替换泛型为具体类型后的方法类型, 通常和 samMethodType 一样, 不同的情况为泛型:
- 比如函数接口方法定义为 void accept(T t) T为泛型标识, 这个时候方法类型为(Object)Void。
- 在编译时T已确定, 即T由String替换, 这时samMethodType就是 (Object)Void,
- instantiatedMethodType为(String)Void。
第4,5,6 三个参数来自class文件中的。如上面引导方法字节码中Method arguments后面的三个参数就是将应用于4, 5, 6的参数。
Method arguments:
#67 (Ljava/lang/Object;)V
#70 invokestatic Lambda.lambda$0:(Ljava/lang/String;)V
#71 (Ljava/lang/String;)V
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();
}
在buildCallSite的函数中
CallSite buildCallSite() throws LambdaConversionException {
final Class<?> innerClass = spinInnerClass();
函数spinInnerClass 构建了这个内部类,也就是生成了一个Lambda$$Lambda$1/716157500 这样的内部类,这个类是在运行的时候构建的,并不会保存在磁盘中,如果想看到这个构建的类,可以通过设置环境参数
System.setProperty("jdk.internal.lambda.dumpProxyClasses", ".");
会在你指定的路径 ,当前运行路径上生成这个内部类
静态类
Java在编译表达式的时候会生成lambda$0静态私有类方法,在这个方法里实现了表达式中的方法块 system.out.println(s);
private static void lambda$0(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #46 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_0
4: invokevirtual #50 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
7: return
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 8 0 s Ljava/lang/String;
当然了在上一步通过设置的jdk.internal.lambda.dumpProxyClasses里生成的
Lambda$$Lambda$1.class
public void accept(java.lang.Object);
descriptor: (Ljava/lang/Object;)V
flags: ACC_PUBLIC
Code:
stack=1, locals=2, args_size=2
0: aload_1
1: checkcast #15 // class java/lang/String
4: invokestatic #21 // Method Lambda.lambda$0:(Ljava/lang/String;)V
7: return
RuntimeVisibleAnnotations:
0: #13()
调用了Lambda.lambda$0静态函数,也就是表达式中的函数块
归纳总结
这样就完成的实现了Lambda表达式,
- 使用invokedynamic指令,运行时调用LambdaMetafactory.metafactory动态的生成内部类,实现了接口,
- 内部类里的调用方法块并不是动态生成的,只是在原class里已经编译生成了一个静态的方法,内部类只需要调用该静态方法
结语
最近熬夜整理了一下Java资料,如果需要的自行点击链接领取~
最全学习笔记大厂真题+微服务+MySQL+分布式+SSM框架+Java+Redis+数据结构与算法+网络+Linux+Spring全家桶+JVM+高并发+各大学习思维脑图+面试集合
以上是关于深入底层原理—带你看透Lambda表达式的本质的主要内容,如果未能解决你的问题,请参考以下文章
虚拟机研究系列-「GC本质底层机制」SafePoint的深入分析和底层原理探究指南