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. 未能找到元数据文件“引用的DLL的路径”

    使用VS的时候   偶尔会出现错误 [未能找到元数据文件“引用的DLL的路径”] 但是实际上项目中这些DLL都是做了引用的,甚至你前一天打开还是好好的,睡一觉起来 不知道什么原因 就酱紫了 原因:不详 ...

  2. 【mysql】添加对emoji的支持

    1.简介 涉及无线相关的 MySQL 数据库建议都提前采用 utf8mb4 字符集,避免 emoji 表情符号带来的问题 MySQL Server >  5.5.3 2.配置+升级 当前配置 m ...

  3. sql 把特定数据排在最前面

    感谢www.baidu.com/p/dongfanghong_1 sql大神,简单的语法运用起来简直活了. 第一法] select * from table where name='D' UNION ...

  4. PHP添加Redis模块及连接

    上几篇文章介绍了Redis的安装及使用,下面将介绍php如何添加Redis扩展! php手册并没有提供Redis的类和方法,也没有提供相关的扩展模块,但我们可以在Redis的官网下载PHP的扩展,里面 ...

  5. Hibernate学习笔记整理系列-------一、Hibernate简介

    Hibernate的官网:http://hibernate.org/ 1.1 Hibernate框架的作用 Hibernate框架是一个数据访问框架(也叫持久层框架,可将实体对象变成持久对象).通过H ...

  6. 笔者的编辑语法:MarkDown

    由于博客园里的文章有很多排版不好,一大堆文字堆在一块会影响到阅读. MarkDowm:百科 Markdown 是一种轻量级标记语言,创始人为约翰·格鲁伯(John Gruber).它允许人们“使用易读 ...

  7. html不使用cache数据

    <HEAD>      <META   HTTP-EQUIV="Pragma"   CONTENT="no-cache">     &l ...

  8. 探索 OpenStack 之(11):cinder-api Service 启动过程分析 以及 WSGI / Paste deploy / Router 等介绍

    OpenStack 中的每一个提供 REST API Service 的组件,比如 cinder-api,nova-api 等,其实是一个 WSGI App,其主要功能是接受客户端发来的 HTTP R ...

  9. [转]asp.net的ajax以及json

    本文转自:http://www.cnblogs.com/ensleep/p/3319756.html 来现在这家公司以前,从未接触过webform,以前在学校做的项目是php,java以及asp.ne ...

  10. [麦先生]如何使用AJAX实现按需加载

    按需加载的优势:在实际调查中发现,很多的网民在游览网站时具有明确的指向性,往往在进入主页后直接搜索进入自己需要的商品列表内,如果在客户进入主页时将主页信息全部加载完毕后展示给顾客,会极大的浪费网站资源 ...