JDK8 的 Lambda 表达式 -- 实现原理初探
JDK8 使用一行 Lambda 表达式可以代替先前用匿名类五六行代码所做的事情,那么它是怎么实现的呢?从所周知,匿名类会在编译的时候生成与宿主类带上 $1, $2 的类文件,如写在 TestLambda 中的匿名类产生成类文件是 TestLambda$1.class, TestLambda$2.class 等。
我试验了一下,如果使用的是 Lambda 表达式并不会生成额外的类文件,那么字节码里是什么样子的?来看下用 javap -c 反编译出下面文件产生的 TestLambda.class,两个方法,一个是 byAnonymousClass() 使用匿名类,另一个是 byLambda 使用 Lambda 的方式:
相应的字节码如下:
对比后我们发现,匿名类的方式会创建一个匿名类(这是废话),如编译出的的 TestLambda$1.class 文件,在磁盘上能看到 TestLambda$1.class 文件
而 Lambda 的方式则不会产生额外的类文件,我们可以让 TestLambda 只保留 byLambda() 方法,就会发现编译后只会有 TestLambda.class 文件。
对比方法调用指令,byLambda 中使用了一个 JDK7 新加的
具体到我们的例子,也就是说对于这个例子虚拟机会在执行 byLambda 的 invokedynamic #25, 0 指令时动态的在内存中创建一个类似与 TestLambda$1 的类,名字可能是 TestLambda$1$$Lambda$1。
为此,我专门做了个实验,下面的代码编译会生成两个类文件,TestLambda.class,TestLambda$1.class, 第二个类文件是由 new ActionListener() 时创建的匿名类。 然后执行下面的代码:
JDK 给我们提供了不少分析 JVM 的工具,如 jps, jinfo, jstack, jmap, jhat, jconsole, jvisualvm 等。
命令:
用 jhat 浏览 before_click.dump,即点击按钮之前的快照,看到的是:

只加载了两个类,TestLambda 和匿名类 TestLambda$1。点击 class cc.unmi.testjdk8.TestLambda$1, 看到 TestLambda$1 中是说继承自 Object, 并未告知它与 ActionListner 有何实现上的关系。

再用 jhat 打开点击按钮执行了 Lambda 表达式后的快照 after_click.dump,这时候就会发现多一个类 cc.unmi.testjdk8.TestLambda$1$$Lambda$1

在磁盘上并不存在这个文件,这是在执行 Lambda 表达式时内存中动态生成的,点击它看看它的父类是什么

在 JDK8 正式版中,它的父类变成了 java.lang.Object。
我试验了一下,如果使用的是 Lambda 表达式并不会生成额外的类文件,那么字节码里是什么样子的?来看下用 javap -c 反编译出下面文件产生的 TestLambda.class,两个方法,一个是 byAnonymousClass() 使用匿名类,另一个是 byLambda 使用 Lambda 的方式:
1package cc.unmi.testjdk8;
2
3import java.awt.event.ActionEvent;
4import java.awt.event.ActionListener;
5
6import javax.swing.JButton;
7
8public class TestLambda{
9 private JButton button = new JButton();
10
11 public void byLambda() {
12 button.addActionListener((ActionEvent e) -> System.out.println("Lambda"));
13 }
14
15 public void byAnonymousClass(){
16
17 button.addActionListener(new ActionListener() {
18 @Override
19 public void actionPerformed(ActionEvent e) {
20 System.out.println("Anonymous class");
21 }
22 });
23 }
24}相应的字节码如下:
1public class cc.unmi.testjdk8.TestLambda {
2 public cc.unmi.testjdk8.TestLambda();
3 Code:
4 0: aload_0
5 1: invokespecial #10 // Method java/lang/Object."<init>":()V
6 4: aload_0
7 5: new #12 // class javax/swing/JButton
8 8: dup
9 9: invokespecial #14 // Method javax/swing/JButton."<init>":()V
10 12: putfield #15 // Field button:Ljavax/swing/JButton;
11 15: return
12
13 public void byLambda();
14 Code:
15 0: aload_0
16 1: getfield #15 // Field button:Ljavax/swing/JButton;
17 4: invokedynamic #25, 0 // InvokeDynamic #0:actionPerformed:()Ljava/awt/event/ActionListener;
18 9: invokevirtual #26 // Method javax/swing/JButton.addActionListener:(Ljava/awt/event/ActionListener;)V
19 12: return
20
21 public void byAnonymousClass();
22 Code:
23 0: aload_0
24 1: getfield #15 // Field button:Ljavax/swing/JButton;
25 4: new #31 // class cc/unmi/testjdk8/TestLambda$1
26 7: dup
27 8: aload_0
28 9: invokespecial #33 // Method cc/unmi/testjdk8/TestLambda$1."<init>":(Lcc/unmi/testjdk8/TestLambda;)V
29 12: invokevirtual #26 // Method javax/swing/JButton.addActionListener:(Ljava/awt/event/ActionListener;)V
30 15: return
31}对比后我们发现,匿名类的方式会创建一个匿名类(这是废话),如编译出的的 TestLambda$1.class 文件,在磁盘上能看到 TestLambda$1.class 文件
而 Lambda 的方式则不会产生额外的类文件,我们可以让 TestLambda 只保留 byLambda() 方法,就会发现编译后只会有 TestLambda.class 文件。
对比方法调用指令,byLambda 中使用了一个 JDK7 新加的
invokedynamic 虚拟机指令。invokedynamic 就是个关键,这里不去深挖,只简单说明,总之它对于 Java 进行函数式编程,增强了语言的动态性意义匪浅,它重新引入了像 C 里函数指针类似的方法句柄的概念。JDK7 之前的方法调用指令有 invokestatic, invokespecial, invokevirtual 和 invokeinterface 四个,它们都是在编译时就确定了实际调用哪个方法。而 invokedynamic 能让虚拟机在执行到该指令时才去动态的链接,调用实际的方法,所以每个 invokedynamic 就是一个动态调用点。具体到我们的例子,也就是说对于这个例子虚拟机会在执行 byLambda 的 invokedynamic #25, 0 指令时动态的在内存中创建一个类似与 TestLambda$1 的类,名字可能是 TestLambda$1$$Lambda$1。
为此,我专门做了个实验,下面的代码编译会生成两个类文件,TestLambda.class,TestLambda$1.class, 第二个类文件是由 new ActionListener() 时创建的匿名类。 然后执行下面的代码:
1package cc.unmi.testjdk8;
2
3import java.awt.event.ActionEvent;
4import java.awt.event.ActionListener;
5
6import javax.swing.JButton;
7import javax.swing.JFrame;
8
9public class TestLambda{
10 private static final JButton button = new JButton("Click");
11
12 public static void main(String[] args) {
13 JFrame frame = new JFrame();
14 frame.setSize(600, 480);
15 frame.add(button);
16 button.addActionListener(new ActionListener() {
17 @Override
18 public void actionPerformed(ActionEvent e) {
19 button.addActionListener((ActionEvent ae) -> {
20 System.out.println("button clicked.");
21 });
22 }
23 });
24
25 frame.setVisible(true);
26 }
27}JDK 给我们提供了不少分析 JVM 的工具,如 jps, jinfo, jstack, jmap, jhat, jconsole, jvisualvm 等。
命令:
jps #可看到 Java Process ID我们可分别在点击按钮的前后用 jmap 生成快照文件 before_click.dump 和 after_click.dump。在点击按钮之前虚拟机还未真正执行到 Lambda 表达式。
jmap -dump:file=before_click.dump <pid> #click 前堆转储为文件
jmap -dump:file=after_click.dump <pid> #click 后堆转储为文件
jhat before_click.dump #默认在 7000 端口打开 Web 服务浏览 open http://localhost:7000
jhat -port 7001 after_click.dump #open http://localhost:7001
用 jhat 浏览 before_click.dump,即点击按钮之前的快照,看到的是:

只加载了两个类,TestLambda 和匿名类 TestLambda$1。点击 class cc.unmi.testjdk8.TestLambda$1, 看到 TestLambda$1 中是说继承自 Object, 并未告知它与 ActionListner 有何实现上的关系。

再用 jhat 打开点击按钮执行了 Lambda 表达式后的快照 after_click.dump,这时候就会发现多一个类 cc.unmi.testjdk8.TestLambda$1$$Lambda$1

在磁盘上并不存在这个文件,这是在执行 Lambda 表达式时内存中动态生成的,点击它看看它的父类是什么

在 JDK8 正式版中,它的父类变成了 java.lang.Object。
在早先的 JDK8 Beta 版中,它的父类是 MagicLambdaImpl,确实有点名符其实,Magic,以图为证:上面使用的 JDK 工具是 jmap 和 jhat,也可以用 Java VisualVM -- 即 jvisualvm 指令来查看执行 lambda 前后的内存快照。截了两个图:
上图为点击按钮前的快照,可以看到只有两个类
上图是点击按钮后的快照,又多了 cc.unmi.testjdk8.TestLambda$1$$Lambda$1 这个类。
小结:JDK8 在实现 Lambda 时使用了 JDK7 虚拟机开始有的 invokedynamic 方法调用指令,该知使得虚拟机执行到 Lambda 表达式时才动态的去创建相应的实现类,并加载执行。 永久链接 https://yanbin.blog/jdk8-lambda-3-inside/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明]本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。
上面使用的 JDK 工具是 jmap 和 jhat,也可以用 Java VisualVM -- 即 jvisualvm 指令来查看执行 lambda 前后的内存快照。截了两个图:
