Lambda底层原理最强解析

最近再次重温Lambda函数编程这本书的时候,思考起第一次看这本书只是为了了解如何深入使用Lambda,却没有去思考为什么可以Lambda了,话比较绕口~~哈哈

你可能会好奇Java编译器是如何实现Lambda表达式,而Java虚拟机又是如何对它们进行处理的。如果你认为Lambda表达式就是简单地被转换为匿名类,那就太天真了。

由于Lambda表达式提供了函数式接口中抽象方法的实现,这让人有一种感觉,似乎在编译过程中让Java编译器直接将Lambda表达式转换为匿名类更直观。不过,匿名类有着种种不尽如人意的特性,会对应用程序的性能带来负面影响。

1. 匿名类

  • 编译器会为每个匿名类生成一个新的.class文件。这些新生成的类文件的文件名通常以ClassName$1这种形式呈现,其中ClassName是匿名类出现的类的名字,紧跟着一个美元符号和一个数字。生成大量的类文件是不利的,因为每个类文件在使用之前都需要加载和验证,这会直接影响应用的启动性能。如果将Lambda表达式转换为匿名类,每个Lambda表达式都会产生一个新的类文件,这是我们不期望发生的。
  • 每个新的匿名类都会为类或者接口产生一个新的子类型。如果你为了实现一个比较器,使用了一百多个不同的Lambda表达式,这意味着该比较器会有一百多个不同的子类型。这种情况下,JVM的运行时性能调优会变得更加困难。

1.1 字节码

Java的源代码文件会经由Java编译器编译为Java字节码。之后JVM可以执行这些生成的字节码运行应用。编译时,匿名类和Lambda表达式使用了不同的字节码指令。你可以通过下面这条命令查看任何类文件的字节码和常量池:

javap -c -v ClassName

Java 7中旧的格式实现了Function接口的一个实例,代码如下所示

这种方式下,和Function对应,以匿名内部类形式生成的字节码看起来就像下面这样:

这段代码展示了下面这些编译中的细节。

  • 通过字节码操作new,一个InnerClass$1类型的对象被实例化了。与此同时,一个指向新创建对象的引用会被压入栈。
  • dup操作会复制栈上的引用。
  • 接着,这个值会被invokespecial指令处理,该指令会初始化对象。
  • 栈顶现在包含了指向对象的引用,该值通过putfield指令保存到了LambdaBytecode类的f1字段。

InnerClass$1是由编译器为匿名类生成的名字。如果你想要再次确认这一情况,也可以查看InnerClass$1类文件,你可以看到Function接口的实现代码如下:

2. InvokeDynamic压轴出场

现在,我们试着采用Java 8中新提供的Lambda表达式来完成同样的功能。我们会查看下面这段代码清单生成的类文件。

你会看到下面这些字节码指令:

我们已经解释过将Lambda表达式转换为内部匿名类的缺点,通过这段字节码你可以再次确认二者之间巨大的差别。创建额外的类现在被invokedynamic指令替代了。

使用invokedynamic指令的目的略微有别于我们最初介绍的那一种。这个例子中,它被用于延迟Lambda表达式到字节码的转换,最终这一操作被推迟到了运行时。换句话说,以这种方式使用invokedynamic,可以将实现Lambda表达式的这部分代码的字节码生成推迟到运行时。这种设计选择带来了一系列好结果。

  • Lambda表达式的代码块到字节码的转换由高层的策略变成了纯粹的实现细节。它现在可以动态地改变,或者在未来版本中得到优化、修改,并且保持了字节码的后向兼容性。
  • 没有带来额外的开销,没有额外的字段,也不需要进行静态初始化,而这些如果不使用Lambda,就不会实现。
  • 对无状态非捕获型Lambda,我们可以创建一个Lambda对象的实例,对其进行缓存,之后对同一对象的访问都返回同样的内容。这是一种常见的用例,也是人们在Java 8之前就惯用的方式;比如,以static final变量的方式声明某个比较器实例。
  • 没有额外的性能开销,因为这些转换都是必须的,并且结果也进行了链接,仅在Lambda首次被调用时需要转换。其后所有的调用都能直接跳过这一步,直接调用之前链接的实现。

3. 代码生成策略

将Lambda表达式的代码体填入到运行时动态创建的静态方法,就完成了Lambda表达式的字节码转换。无状态Lambda在它涵盖的范围内不保持任何状态信息,字节码转换时它是所有Lambda中最简单的一种类型。这种情况下,编译器可以生成一个方法,该方法含有该Lambda表达式同样的签名,所以最终转换的结果从逻辑上看起来就像下面这样:

Lambda表达式中包含了final(或者效果上等同于final)的本地变量或者字段的情况会稍微复杂一些,就像下面的这个例子:

这个例子中,生成方法的签名不会和Lambda表达式一样,因为它还需要携带参数来传递上下文中额外的状态。为了实现这一目标,最简单的方案是在Lambda表达式中为每一个需要额外保存的变量预留参数,所以实现前面Lambda表达式的生成方法会像下面这样:

猜你喜欢

转载自blog.csdn.net/wmq880204/article/details/115022727
今日推荐