2014年3月,Java 8发布,Lambda表达式作为一项重要的特性随之而来。或许现在你已经在使用Lambda表达式来书写简洁灵活的代码。比如,你可以使用Lambda表达式和新增的流相关的API,完成如下的大量数据的查询处理:


int total = invoices.stream()
.filter(inv -> inv.getMonth() == Month.JULY)
.mapToInt(Invoice::getAmount)
.sum();

上面的示例代码描述了如何从一打发票中计算出7月份的应付款总额。其中我们使用Lambda表达式过滤出7月份的发票,使用方法引用来提取出发票的金额。

到这里,你可能会对Java编译器和JVM内部如何处理Lambda表达式和方法引用比较好奇。可能会提出这样的问题,Lambda表达式会不会就是匿名内部类的语法糖呢?毕竟上面的示例代码可以使用匿名内部类实现,将Lambda表达式的方法体实现移到匿名内部类对应的方法中即可,但是我们并不赞成这样做。如下为匿名内部类实现版本:


int total = invoices.stream()
.filter(new Predicate<Invoice>() {
@Override
public boolean test(Invoice inv) {
return inv.getMonth() == Month.JULY;
}
})
.mapToInt(new ToIntFunction<Invoice>() {
@Override
public int applyAsInt(Invoice inv) {
return inv.getAmount();
}
})
.sum();

本文将会介绍为什么Java编译器没有采用内部类的形式处理Lambda表达式,并解密Lambda表达式和方法引用的内部实现。接着介绍字节码生成并简略分析Lambda表达式理论上的性能。最后,我们将讨论一下实践中Lambda表达式的性能问题。

为什么匿名内部类不好?

实际上,匿名内部类存在着影响应用性能的问题。

首先,编译器会为每一个匿名内部类创建一个类文件。创建出来的类文件的名称通常按照这样的规则 ClassName符合和数字。生成如此多的文件就会带来问题,因为类在使用之前需要加载类文件并进行验证,这个过程则会影响应用的启动性能。类文件的加载很有可能是一个耗时的操作,这其中包含了磁盘IO和解压JAR文件。

假设Lambda表达式翻译成匿名内部类,那么每一个Lambda表达式都会有一个对应的类文件。随着匿名内部类进行加载,其必然要占用JVM中的元空间(从Java 8开始永久代的一种替代实现)。如果匿名内部类的方法被JIT编译成机器代码,则会存储到代码缓存中。同时,匿名内部类都需要实例化成独立的对象。以上关于匿名内部类的种种会使得应用的内存占用增加。因此我们有必要引入新的缓存机制减少过多的内存占用,这也就意味着我们需要引入某种抽象层。

最重要的,一旦Lambda表达式使用了匿名内部类实现,就会限制了后续Lambda表达式实现的更改,降低了其随着JVM改进而改进的能力。

我们看一下下面的这段代码:


import java.util.function.Function;
public class AnonymousClassExample {
Function<String, String> format = new Function<String, String>() {
public String apply(String input){
return Character.toUpperCase(input.charAt(0)) + input.substring(1);
}
};
}

使用这个命令我们可以检查任何类文件生成的字节码


javap -c -v ClassName

示例中使用Function创建的匿名内部类对应的字节码如下:


0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: new #2 // class AnonymousClassExample$1
8: dup
9: aload_0
10: invokespecial #3 // Method AnonymousClass$1."<init>":(LAnonymousClassExample;)V
13: putfield #4 // Field format:Ljava/util/function/Function;
16: return

上述字节码的含义如下:

  • 第5行,使用字节码操作new创建了类型AnonymousClassExample$1的一个对象,同时将新创建的对象的的引用压入栈中。
  • 第8行,使用dup操作复制栈上的引用。
  • 第10行,上面的复制的引用被指令invokespecial消耗使用,用来初始化匿名内部类实例。
  • 第13行,栈顶依旧是创建的对象的引用,这个引用通过putfield指令保存到AnonymousClassExample类的format属性中。

AnonymousClassExample1这个类文件,你会发现这个类就是Function接口的实现。

将Lambda表达式翻译成匿名内部类会限制以后可能进行的优化(比如缓存)。因为一旦使用了翻译成匿名内部类形式,那么Lambda表达式则和匿名内部类的字节码生成机制绑定。因而,Java语言和JVM工程师需要设计一个稳定并且具有足够信息的二进制表示形式来支持以后的JVM实现策略。下面的部分将介绍不使用匿名内部类机制,Lambda表达式是如何工作的。

Lambdas表达式和invokedynamic

为了解决前面提到的担心,Java语言和JVM工程师决定将翻译策略推迟到运行时。利用Java 7引入的invokedynamic字节码指令我们可以高效地完成这一实现。将Lambda表达式转化成字节码只需要如下两步:

1.生成一个invokedynamic调用点,也叫做Lambda工厂。当调用时返回一个Lambda表达式转化成的函数式接口实例。

2.将Lambda表达式的方法体转换成方法供invokedynamic指令调用。

为了阐明上述的第一步,我们这里举一个包含Lambda表达式的简单类:


import java.util.function.Function; public class Lambda {

Function<String, Integer> f = s -> Integer.parseInt(s);

}

查看上面的类经过编译之后生成的字节码:


0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: invokedynamic #2, 0 // InvokeDynamic
#0:apply:()Ljava/util/function/Function;
10: putfield #3 // Field f:Ljava/util/function/Function;
13: return

需要注意的是,方法引用的编译稍微有点不同,因为javac不需要创建一个合成的方法,javac可以直接访问该方法。

Lambda表达式转化成字节码的第二步取决于Lambda表达式是否为对变量捕获。Lambda表达式方法体需要访问外部的变量则为对变量捕获,反之则为对变量不捕获。

对于不进行变量捕获的Lambda表达式,其方法体实现会被提取到一个与之具有相同签名的静态方法中,这个静态方法和Lambda表达式位于同一个类中。比如上面的那段Lambda表达式会被提取成类似这样的方法:


static Integer lambda$1(String s) {
return Integer.parseInt(s);
}

需要注意的是,这里的$1并不是代表内部类,这里仅仅是为了展示编译后的代码而已。

对于捕获变量的Lambda表达式情况有点复杂,同前面一样Lambda表达式依然会被提取到一个静态方法中,不同的是被捕获的变量同正常的参数一样传入到这个方法中。在本例中,采用通用的翻译策略预先将被捕获的变量作为额外的参数传入方法中。比如下面的示例代码:


int offset = 100;
Function<String, Integer> f = s -> Integer.parseInt(s) + offset;

对应的翻译后的实现方法为:


static Integer lambda$1(int offset, String s) {
return Integer.parseInt(s) + offset;
}

需要注意的是编译器对于Lambda表达式的翻译策略并非固定的,因为这样invokedynamic可以使编译器在后期使用不同的翻译实现策略。比如,被捕获的变量可以放入数组中。如果Lambda表达式用到了类的实例的属性,其对应生成的方法可以是实例方法,而不是静态方法,这样可以避免传入多余的参数。

性能分析

Lambda表达式最主要的优势表现在性能方面,虽然使用它很轻松的将很多行代码缩减成一句,但是其内部实现却不这么简单。下面对内部实现的每一步进行性能分析。

第一步就是连接,对应的就是我们上面提到的Lambda工厂。这一步相当于匿名内部类的类加载过程。来自Oracle的Sergey Kuksenko发布过相关的性能报告,并且他也在2013 JVM语言大会就该话题做过分享。报告表明,Lambda工厂的预热准备需要消耗时间,并且这个过程比较慢。伴随着更多的调用点连接,代码被频繁调用后(比如被JIT编译优化)性能会提升。另一方面如果连接处于不频繁调用的情况,那么Lambda工厂方式也会比匿名内部类加载要快,最高可达100倍。

第二步就是捕获变量。正如我们前面提到的,如果是不进行捕获变量,这一步会自动进行优化,避免在基于Lambda工厂实现下额外创建对象。对于匿名内部类而言,这一步对应的是创建外部类的实例,为了优化内部类这一步的问题,我们需要手动的修改代码,如创建一个对象,并将它设置给一个静态的属性。如下述代码:


// Hoisted Function
public static final Function<String, Integer> parseInt = new Function<String, Integer>() {
public Integer apply(String arg) {
return Integer.parseInt(arg);
}
}; // Usage:

int result = parseInt.apply(“123”);

第三部就是真实方法的调用。在这一步中匿名内部类和Lambda表达式执行的操作相同,因此没有性能上的差别。不进行捕获的Lambda表达式要比进行static优化过的匿名内部类较优。进行变量捕获的Lambda表达式和匿名内部类表达式性能大致相同。

在这一节中,我们明显可以看到Lambda表达式的实现表现良好,匿名内部类通常需要我们手动的进行优化来避免额外对象生成,而对于不进行变量捕获的Lambda表达式,JVM已经为我们做好了优化。

实践中的性能分析

理解了Lambda的性能模型很是重要,但是实际应用中的总体性能如何呢?我们在使用Java 8 编写了一些软件项目,一般都取得了很好的效果。非变量捕获的Lambda表达式给我们带来了很大的帮助。这里有一个很特殊的例子描述了关于优化方向的一些有趣的问题。

这个例子的场景是代码需要运行在一个要求GC暂定时间越少越好的系统上。因而我们需要避免创建大量的对象。在这个工程中,我们使用了大量的Lambda表达式来实现回调处理。然而在这些使用Lambda实现的回调中很多并没有捕获局部变量,而是需要引用当前类的变量或者调用当前类的方法。然而目前仍需要对象分配。下面就是我们提到的例子的代码:


public MessageProcessor() {} public int processMessages() {

return queue.read(obj -> {

if (obj instanceof NewClient) {

this.processNewClient((NewClient) obj);

}

...

});

}

有一个简单的办法解决这个问题,我们将Lambda表达式的代码提前到构造方法中,并将其赋值给一个成员属性。在调用点我们直接引用这个属性即可。下面就是修改后的代码:


private final Consumer<Msg> handler; public MessageProcessor() {

handler = obj -> {

if (obj instanceof NewClient) {

this.processNewClient((NewClient) obj);

}

...

};

} public int processMessages() {

return queue.read(handler);

}

然而上面的修改后代码给却给整个工程带来了一个严重的问题:性能分析表明,这种修改产生很大的对象申请,其产生的内存申请在总应用的60%以上。

类似这种无关上下文的优化可能带来其他问题。

  1. 纯粹为了优化的目的,使用了非惯用的代码写法,可读性会稍差一些。
  2. 内存分配方面的问题,示例中为MessageProcessor增加了一个成员属性,使得MessageProcessor对象需要申请更大的内存空间。Lambda表达式的创建和捕获位于构造方式中,使得MessageProcessor的构造方法调用缓慢一些。

我们遇到这种情况,需要进行内存分析,结合合理的业务用例来进行优化。有些情况下,我们使用成员属性确保为经常调用的Lambda表达式只申请一个对象,这样的缓存策略大有裨益。任何性能调优的科学的方法都可以进行尝试。

上述的方法也是其他程序员对Lambda表达式进行优化应该使用的。书写整洁,简单,函数式的代码永远是第一步。任何优化,如上面的提前代码作为成员属性,都必须结合真实的具体问题进行处理。变量捕获并申请对象的Lambda表达式并非不好,就像我们我们写出new Foo()代码并非一无是处一样。

除此之外,我们想要写出最优的Lambda表达式,常规书写很重要。如果一个Lambda表达式用来表示一个简单的方法,并且没有必要对上下文进行捕获,大多数情况下,一切以简单可读即可。

总结

在这片文章中,我们研究了Lambda表达式不是简单的匿名内部类的语法糖,为什么匿名内部类不是Lambda表达式的内部实现机制以及Lambda表达式的具体实现机制。对于大多数情况来说,Lambda表达式要比匿名内部类性能更优。然而现状并非完美,基于测量驱动优化,我们仍然有很大的提升空间。

Lambda表达式的这种实现形式并非Java 8 所有。Scala曾经通过生成匿名内部类的形式支持Lambda表达式。在Scala 2.12版本,Lambda的实现形式替换为Java 8中的Lambda 工厂机制。后续其他可以在JVM上运行的语言也可能支持Lambda的这种机制。

原文:Java 8 Lambdas - A Peek Under the Hood

查看英文原文:Java 8 Lambdas - A Peek Under the Hood


深入探索Java 8 Lambda表达式的更多相关文章

  1. Java 8 Lambda表达式

    Java 8 Lambda表达式探险 http://www.cnblogs.com/feichexia/archive/2012/11/15/Java8_LambdaExpression.html 为 ...

  2. 深入浅出 Java 8 Lambda 表达式

    摘要:此篇文章主要介绍 Java8 Lambda 表达式产生的背景和用法,以及 Lambda 表达式与匿名类的不同等.本文系 OneAPM 工程师编译整理. Java 是一流的面向对象语言,除了部分简 ...

  3. Java 8 Lambda表达式10个示例【存】

    PS:不能完全参考文章的代码,请参考这个文件http://files.cnblogs.com/files/AIThink/Test01.zip 在Java 8之前,如果想将行为传入函数,仅有的选择就是 ...

  4. Java 8 Lambda 表达式

    Lambda 是啥玩意 简单来说,Lambda 就是一个匿名的方法,就这样,没啥特别的.它采用一种非常简洁的方式来定义方法.当你想传递可复用的方法片段时,匿名方法非常有用.例如,将一个方法传递给另外一 ...

  5. Java 8 lambda表达式示例

    例1.用lambda表达式实现Runnable 我开始使用Java 8时,首先做的就是使用lambda表达式替换匿名类,而实现Runnable接口是匿名类的最好示例.看一下Java 8之前的runna ...

  6. Java 8 Lambda 表达式详解

    一.Java 8 Lambda 表达式了解 参考:Java 8 Lambda 表达式 | 菜鸟教程 1.1 介绍: Lambda 表达式,也可称为闭包,是推动 Java 8 发布的最重要新特性. La ...

  7. 用Java 8 Lambda表达式实现设计模式:命令模式

    在这篇博客里,我将说明如何在使用 Java 8 Lambda表达式 的函数式编程方式 时实现 命令 设计模式 .命令模式的目标是将请求封装成一个对象,从对客户端的不同类型请求,例如队列或日志请求参数化 ...

  8. Java基础学习总结(44)——10个Java 8 Lambda表达式经典示例

    Java 8 刚于几周前发布,日期是2014年3月18日,这次开创性的发布在Java社区引发了不少讨论,并让大家感到激动.特性之一便是随同发布的lambda表达式,它将允许我们将行为传到函数里.在Ja ...

  9. 02、Java的lambda表达式和JavaScript的箭头函数

    前言 在JDK8和ES6的语言发展中,在Java的lambda表达式和JavaScript的箭头函数这两者有着千丝万缕的联系:本次试图通过这篇文章弄懂上面的两个"语法糖". 简介 ...

随机推荐

  1. 问题解决——Group Box控件遮挡其他控件

    转载请保持文章的完整性并显要地注明出处 本文链接:http://blog.csdn.net/wlsgzl/article/details/38042301 ====================== ...

  2. C++基础——子类转父类转子类 (派生类转基类转派生类)

    ==================================声明================================== 本文原创,转载在正文中显要的注明作者和出处,并保证文章的完 ...

  3. POJ 2513 Colored Sticks(欧拉回路,字典树,并查集)

    题意:给定一些木棒,木棒两端都涂上颜色,求是否能将木棒首尾相接,连成一条直线,要求不同木棒相接的一边必须是相同颜色的.   无向图存在欧拉路的充要条件为: ①     图是连通的: ②     所有节 ...

  4. 17082 两个有序数序列中找第k小

    17082 两个有序数序列中找第k小 时间限制:1000MS  内存限制:65535K 提交次数:0 通过次数:0 题型: 编程题   语言: 无限制 Description 已知两个已经排好序(非减 ...

  5. multiprocessing module in python(转)

    序.multiprocessing python中的多线程其实并不是真正的多线程,如果想要充分地使用多核CPU的资源,在python中大部分情况需要使用多进程.Python提供了非常好用的多进程包mu ...

  6. C++ transform

    transform函数的作用是:将某操作应用于指定范围的每个元素.transform函数有两个重载版本: transform(first,last,result,op);//first是容器的首迭代器 ...

  7. SSH web.xml文件配置

    启动一个WEB项目的时候, WEB容器会去读取它的配置文件web.xml web.xml中配置的加载优先级:context-param -> listener -> filter -> ...

  8. Eclipse中修改SVN用户名和密码方法(转)

    由于在svn 的界面中并没有为我们提供直接更换用户名密码的地方,所以一旦我们需要更换用户名的就需要自己想一些办法. 解决方案: 在Eclipse 使用SVN 的过程中大多数人往往习惯把访问SVN 的用 ...

  9. 《致命接触》:人畜共患传染病的故事,SARS一章非常精彩,四星推荐

    讲人畜共患的传染病的故事:亨德拉.埃博拉.疟疾.SARS.Q热.鹦鹉热.莱姆病.艾滋病等.作者比较会讲故事,又熟悉病毒传播相关的各种学科的知识(病毒学.生态学,还有一些我没记住的相关小学科),能把相关 ...

  10. [转]JavaScript实现 页面滚动图片加载

    本文转自:http://www.cnblogs.com/Darren_code/archive/2011/07/21/LoadImage.html 又到了这个月的博客时间了,原计划是打算在这个月做一个 ...