转眼间从java8引入的lambda也已经不再是个新鲜玩意儿了,然而笔者对它却是熟悉又陌生。网上已经有很多大佬写的相关文章,笔者今天就站在巨人们的肩膀上简单分析一下,嘿嘿嘿。
可惜水平有限,有错误的地方还望老哥们指正TT
匿名内部类
谈及lambda,就不得不提及我们在java中多次使用的匿名内部类。在lambda出现之前,作为各种回调的主要载体承载了我们的血汗。当然在今天他也同样重要,很多种情况下依然是我们的不二之选,然而在有些情况下确实可以被lambda所替代简化。那本文就先从它开刀。
public class NewTest {
Runnable r0 = new Runnable() { //普通的匿名内部类
@Override
public void run() { }
};
}
上文就是一个简单的匿名内部类,编译之后,会多出一个NewTest$1.class的文件,这个就是我们普通的匿名内部类生成的文件
使用 javap -p NewTest\$1.class
查看
Compiled from "NewTest.java"
class NewTest$1 implements java.lang.Runnable {
final NewTest this$0;
NewTest$1(NewTest);
public void run();
}
显而易见的,其持有了外部类引用:this$0。也是我们开发中造成内存泄漏的一个原因。
那么,我们的lambda是否会有一些不同呢?是否只是单纯简化了匿名内部类的写法呢?
lambda
分析lambda之前,我们简单了解一下java7引入的一个概念,MethodHandle
顾名思义,代表对一个java方法的持有,可以通过invoke等方法对其持有的java方法调用,相对反射来说,更安全更快。本文只简单梳理lambda流程,想要详细了解的老哥可以去查相关资料。本文
那么,
客官里边儿请~
先将刚才的代码改成lambda写法
public class NewTest {
Runnable r1 = ()->{ };
}
编译后通过javap -v -p NewTest.class
输出class文件的详细信息
因为内容较多,这里分段分析。
首先看一下构造方法。
public NewTest();
···
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: invokedynamic #2, 0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
10: putfield #3 // Field r1:Ljava/lang/Runnable;
13: return
···
首先通过invokespecial指令init实例,接下来则调用了刚刚提到的invokedynamic指令。
这个指令是干什么的呢?偷偷查了一下
每一处含有invokedynamic指令的位置都称做“动态调用点”(Dynamic Call Site),这条指令的第一个参数不再是代表方法符号引用的CONSTANT_Methodref_info常量,而是变为JDK 1.7新加入的CONSTANT_InvokeDynamic_info常量,从这个新常量中可以得到3项信息:引导方法(Bootstrap Method,此方法存放在新增的BootstrapMethods属性中)、方法类型(MethodType)和名称。引导方法是有固定的参数,并且返回值是java.lang.invoke.CallSite对象,这个代表真正要执行的目标方法调用。根据CONSTANT_InvokeDynamic_info常量中提供的信息,虚拟机可以找到并且执行引导方法,从而获得一个CallSite对象,最终调用要执行的目标方法。
简单的说,invokedynamic指令通过存放在BootstrapMethods中的引导方法(MethodHandle)获得一个CallSite对象。这个CallSite对象也持有了一个MethodHandle。通过对这个CallSite对象的MethodHandle,获得我们要的最终实例,在这里也就是Runnable实例。
我们按照顺序分析
- 首先根据指令的第一个参数获取对应的CONSTANT_InvokeDynamic_info常量以及其包含的信息:引导方法,方法类型,名称。
invokedynamic #2, 0
//先去常量池中查找对应的CONSTANT_InvokeDynamic_info常量
#2 = InvokeDynamic #0:#23
//这两个参数,第一个#0代表了存在BootstrapMethods中的引导方法,等下再看,第二个#23代表方法类型和名称,继续去常量池中查找
#23 = NameAndType #29:#30
#29 = Utf8 run
#30 = Utf8 ()Ljava/lang/Runnable
//正如我们刚刚代码中写的,此lambda实现的是Runnable的run方法 - 引导方法
引导方法前三个参数是固定的,后面还可以附加任意数量的参数,但是参数的类型是有限制的
BootstrapMethods:
0: #20 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:
#21 ()V
#22 invokestatic NewTest.lambda$new$0:()V
#21 ()V
这里的引导方法是java/lang/invoke/LambdaMetafactory.metafactory,自带了三个参数:
#21 ()V //我们要实现方法的(参数类型)返回类型
#22 invokestatic NewTest.lambda$new$0:()V //我们自己写的lambda实现的方法
#21 ()V //也是(参数类型)返回类型 ,但有泛形的形况下会不同,这里是会是具体的类型描述,上一个则是Ljava/lang/Object
方法返回值就是上面提到的CallSite类型。
- 虚拟机最终通过CallSite.makeSite方法来调用作为引导方法的MethodHandle的invoke(或invokeExact)方法,获得CallSite对象。这里我们的引导方法就是LambdaMetafactory.metafactory方法。下面我们简单分析一下这个方法。
LambdaMetafactory::metafactory
public static CallSite metafactory(MethodHandles.Lookup caller,
String invokedName,//引用方法名,这里是run
MethodType invokedType,//引用方法类型,这里是Runnable
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();
}
首先构建一个Lambda元工厂,在通过此原工厂生成CallSite对象返回。
InnerClassLambdaMetafactory::buildCallSite
CallSite buildCallSite() throws LambdaConversionException {
final Class<?> innerClass = spinInnerClass();
if (invokedType.parameterCount() == 0) {
final Constructor<?>[] ctrs = AccessController.doPrivileged(
new PrivilegedAction<Constructor<?>[]>() {
@Override
public Constructor<?>[] run() {
Constructor<?>[] ctrs = innerClass.getDeclaredConstructors();
if (ctrs.length == 1) {
// The lambda implementing inner class constructor is private, set
// it accessible (by us) before creating the constant sole instance
ctrs[0].setAccessible(true);
}
return ctrs;
}
});
```
try {
Object inst = ctrs[0].newInstance();
return new ConstantCallSite(MethodHandles.constant(samBase, inst));
}
```
} else {
```
}
}
- 通过spinInnerClass方法生成一个暂时我们也不知道是啥的Class对象
- 获取该Class的构造方法,生成该类的实例inst。
- 使用MethodHandles.constant方法生成对应的MethodHandle,这个MethodHandle的作用就是总是返回我们传进去的对象实例inst,使用CallSite包装并返回。
这里的重点应该就是那个我们也不知道是啥的类了嘿嘿嘿。
InnerClassLambdaMetafactory::spinInnerClass
这个方法有点长,简单的说就是
根据生成此Lambda元工厂时设置的各种相关信息,通过ClassWriter生成对应的byte数组,最后通过UNSAFE.defineAnonymousClass注入得到对应的Class。因为是运行期间生成的,我们也看不到对应的class文件,咋办呢?
在这个方法中,有一段代码
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"));
}
可以看到如果dumper!=null,就会把生成的文件输出了。那么,如何设置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);
}
可以看到通过设置jdk.internal.lambda.dumpProxyClasses->path则会生成dumper实例。
这里我改了一下之前的代码
public class NewTest {
public static void main(String[] args) {
System.getProperties().put("jdk.internal.lambda.dumpProxyClasses", "src");
Runnable r1 = ()->{ };
}
}
运行即可在主目录输出我们重要的class文件了,用idea反编译看看
final class NewTest$$Lambda$1 implements Runnable {
private NewTest$$Lambda$1() {
}
@Hidden
public void run() {
NewTest.lambda$main$0();
}
}
可以看到此类继承了我们的Runnable,实现了run方法。但是run方法的实现并不是我们在代码中写的,我们根本就是写的空实现啊。
那么,回过头来,再看看引导方法的倒数第二个参数:
#22 invokestatic NewTest.lambda$new$0:()V
正是生成的class类run方法的实现!
( 你们不要说我睁着眼睛说瞎话TT因为后面设置输出路径的时候更改了代码,把lambda的声明给放到main里面去了,所以名字长得不一样。。都写到这了我是撒泼不想改了,就是一个东西嘿嘿嘿。 拍胸脯.gif
那么这个方法在那里呢?
回到我们生成的NewTest的字节码信息中看,发现了这个方法
private static void lambda$main$0();
descriptor: ()V
flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
Code:
stack=0, locals=0, args_size=0
0: return
LineNumberTable:
line 16: 0
果然方法里啥也没干!就是我们写的lambda实现~。
读到这里我们就大致梳理完了,总结一下:
1.查找引导方法
2.通过Callsite.makeSite方法创建对应的class类并实例化,将其用Callsite及MethodHandle包装后返回。
3.通过对callsite的调用获得刚才创建的对应的类的实例(这一步我并没有找到证据,网上看来的TT,不过通过debug获得的的确是运行时创建的类的实例)
最后
Lambda好处都有啥?谁说对了就给他~