Java8 Lambda和Invokedynamic详情

目录
  • 一、阐明lambda
  • 二、匿名内部类
  • 三、Lambdas和Invokedynamic
  • 四、性能表现

一、阐明lambda

Java8于2014年3月发布,并引入了lambda表达式作为其旗舰功能。我们可能已经在代码库中使用它们来编写更简洁、更灵活的代码。例如,我们可以将lambda表达式与新的Streams API结合起来,以表达丰富的数据处理查询:

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

此示例显示如何从发票集合中计算7月份到期的总金额。传递lambda表达式以查找月份为7月的发票,并传递方法引用以从发票中提取金额。

您可能想知道Java编译器如何在幕后实现lambda表达式和方法引用,以及Java虚拟机(JVM)如何处理它们。例如,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性能。最后,我们将讨论现实世界中的性能影响。

二、匿名内部类

匿名内部类具有可能影响应用程序性能的不良特征。

首先,编译器为每个匿名内部类生成一个新的类文件。文件名通常看起来像ClassName$1,其中ClassName是定义匿名内部类的类的名称,后跟一个美元符号和一个数字。生成许多类文件是不可取的,因为每个类文件在使用之前都需要加载和验证,这会影响应用程序的启动性能。加载可能是一项昂贵的操作,包括磁盘I/O和解压缩JAR文件本身。

如果将lambda转换为匿名内部类,则每个lambda都会有一个新的类文件。由于每个匿名内部类都将被加载,因此它将占用JVM元空间的空间(这是永久生成的Java8替代品)。如果JVM将每个匿名内部类中的代码编译成机器代码,那么它将存储在代码缓存中。此外,这些匿名内部类将被实例化为单独的对象。因此,匿名内部类会增加应用程序的内存消耗。引入缓存机制以减少所有这些内存开销可能会有所帮助,这促使引入某种抽象层。

最重要的是,从第一天起选择使用匿名内部类实现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

为作为匿名内部类创建的函数生成的相应字节码如下所示:

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实例化匿名类示例$1类型的对象。同时在堆栈上推送对新创建对象的引用。
  • 8:dup操作在堆栈上复制该引用。
  • 10:然后,该值由invokespecial指令使用,该指令初始化匿名内部类实例。
  • 13:堆栈顶部现在仍然包含对对象的引用,该引用使用putfield指令存储在AnonymousClassExample类的format字段中。

AnonymousClassExample$1是编译器为匿名内部类生成的名称。如果您想让自己放心,还可以检查AnonymousClassExample$1类文件,您将找到函数接口实现的代码。

lambda表达式转换为匿名内部类将限制未来可能的优化(例如缓存),因为它们与匿名内部类字节码生成机制相关联。因此,语言和JVM工程师需要一个稳定的二进制表示,该表示提供了足够的信息,同时允许JVM在将来使用其他可能的实现策略。下一节将解释这是如何实现的!

三、Lambdas和Invokedynamic

为了解决上一节中解释的问题,Java语言和JVM工程师决定将转换策略的选择推迟到运行时。Java7引入的新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不需要生成合成方法,可以直接引用该方法。

第二步的执行方式取决于lambda表达式是非捕获(lambda不访问在其主体外部定义的任何变量)还是捕获(lambda访问在其主体外部定义的变量)。

非捕获lambda被简单地分解为一个静态方法,该方法具有与lambda表达式完全相同的签名,并在使用lambda表达式的同一类中声明。例如,可以将上面lambda类中声明的lambda表达式分解为如下方法:

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

注意:$1不是一个内部类,它只是我们表示编译器生成代码的方式

捕获lambda表达式的情况稍微复杂一些,因为捕获的变量必须与lambda的形式参数一起传递给实现lambda表达式主体的方法。在这种情况下,常见的转换策略是在lambda表达式的参数前面加上每个捕获变量的附加参数。让我们看一个实际的例子:

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

相应的方法实现可以通过asy生成:

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

然而,这种转换策略并不是一成不变的,因为invokedynamic指令的使用使编译器能够灵活地在将来选择不同的实现策略。例如,捕获的值可以装箱到数组中,或者,如果lambda表达式读取使用它的类的某些字段,则生成的方法可以是实例方法,而不是声明为静态的,从而避免将这些字段作为附加参数传递。

四、性能表现

这种方法的主要优点是性能特性。如果把它们看作是可以简化为一个数字,那就太好了,但实际上这里涉及到多个操作。

第一步是联动步骤,与上述lambda工厂步骤相对应。如果我们将性能与匿名内部类进行比较,那么等效的操作将是匿名内部类的类加载。Oracle已经发布了Sergey Kuksenko对这一权衡的性能分析,您可以看到Kuksenko在2013年JVM语言峰会上就这一主题发表了演讲[3]。分析表明,需要时间来预热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表达式的开箱即用性能已经领先于提升的匿名内部类。捕获lambda表达式的实现与分配匿名内部类以捕获这些字段的性能类似。

我们在本节中看到,lambda表达式的实现大体上表现良好。虽然匿名内部类需要手动优化以避免分配,但JVM已经为我们优化了最常见的情况(一个不捕获其参数的lambda表达式)。

当然,理解整体性能模型是很好的,但是在实践中,事情是如何叠加的呢?我们已经在一些软件项目中使用了Java8,并取得了积极的成果。自动优化非捕获lambda可以提供很好的好处。这里有一个特别的例子,它提出了一些关于未来优化方向的有趣问题。

所讨论的示例发生在处理某些代码以供系统使用时,该系统需要特别低的GC暂停,理想情况下没有。因此,希望避免分配太多的对象。该项目广泛使用lambdas来实现回调处理程序。不幸的是,我们仍然有相当多的回调,其中我们没有捕获局部变量,但希望引用当前类的字段,甚至只调用当前类上的方法。目前,这似乎仍然需要分配。下面是一个代码示例,旨在阐明我们所讨论的内容:

public MessageProcessor() {} 

public int processMessages() {
    return queue.read(obj -> {
        if (obj instanceof NewClient) {
            this.processNewClient((NewClient) obj);
        }
        ...
    });
}

这个问题有一个简单的解决办法。我们将代码提升到构造函数中,并将其分配给一个字段,然后在调用站点直接引用该字段。下面是我们之前重写的代码示例:

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%以上。

与任何潜在的优化一样,无论环境如何,应用这种方法都可能会带来其他问题。

您选择编写非惯用代码纯粹是出于性能原因。因此有一个可读性权衡

这也关系到分配的权衡。您正在向MessageProcessor添加一个字段,使其更大,以便分配。相关lambda的创建和捕获也会减慢对MessageProcessor的构造函数调用。

我们不是通过寻找场景,而是通过内存分析发现了这种情况,并且有一个很好的业务用例证明了优化的合理性。我们还处于这样一个位置:对象只分配一次,大量重用lambda表达式,因此缓存非常有益。与任何性能调整练习一样,通常推荐使用科学方法。

这也是任何其他最终用户寻求优化其lambda表达式使用的方法。尝试编写干净、简单且功能强大的代码始终是最好的第一步。任何优化,如本次吊装,应仅针对真正的问题进行。编写捕获分配对象的lambda表达式本身并不坏——正如编写调用'new Foo()'的Java代码本身也不坏一样。

这一经验也确实表明,要充分利用lambda表达式,重要的是要习惯地使用它们。如果lambda表达式用于表示小的纯函数,则它们几乎不需要从其周围范围捕获任何内容。和大多数事情一样,如果你保持简单,事情就会表现得很好。

结论
在本文中,我们解释了lambda不仅仅是隐藏的匿名内部类,以及为什么匿名内部类不是lambda表达式的合适实现方法。通过lambda表达式实现方法,已经进行了大量的工作。目前,对于大多数任务,它们都比匿名内部类快,但当前的状态并不完美;测量驱动的手动优化仍有一定的空间。

Java8中使用的方法不仅仅局限于Java本身。Scala历来通过生成匿名内部类来实现其lambda表达式。在Scala2.12中,我们已经开始使用Java8中引入的lambda元工厂机制。随着时间的推移,JVM上的其他语言也可能采用这种机制。

到此这篇关于Java8 LambdaInvokedynamic详情的文章就介绍到这了,更多相关Java8 LambdaInvokedynamic内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java8 lambda表达式2种常用方法代码解析

    与python不一样,python lambda是定义匿名函数,而在java8中lambda是匿名内部类 例1.用lambda表达式实现Runnable 我开始使用Java 8时,首先做的就是使用lambda表达式替换匿名类,而实现Runnable接口是匿名类的最好示例.看一下Java 8之前的runnable实现方法,需要4行代码,而使用lambda表达式只需要一行代码.我们在这里做了什么呢?那就是用() -> {}代码块替代了整个匿名类. // Java 8之前: new Thread(ne

  • Java8 Lambda表达式模板方法实现解析

    Java注解提供了关于代码的一些信息,但并不直接作用于它所注解的代码内容.在这个教程当中,我们将学习Java的注解,如何定制注解,注解的使用以及如何通过反射解析注解. Java1.5引入了注解,当前许多java框架中大量使用注解,如Hibernate.Jersey.Spring.注解作为程序的元数据嵌入到程序当中.注解可以被一些解析工具或者是编译工具进行解析.我们也可以声明注解在编译过程或执行时产生作用. 在使用注解之前,程序源数据只是通过java注释和javadoc,但是注解提供的功能要远远超

  • java8 forEach结合Lambda表达式遍历 List操作

    我就废话不多说了,大家还是直接看代码吧~ @Test void testJava8ForeachMap() { Map<String, Integer> items = new HashMap<>(); items.put("A", 10); items.put("B", 20); items.put("C", 30); items.put("D", 40); items.put("E&quo

  • Java8新特性之Lambda表达式的使用

    1. lambda表达式介绍 lambda表达式是Java8提供的新特性之一,也可以称之为闭包:它支持Java能够进行简单的函数式编程,也就是说可以把一个匿名函数作为一个方法的参数进行传递:其格式分为三部分,第一部分为入参列表,第二部由->固定组成,第三部分为方法体: public class LambdaTest { public static void main(String[] args) { // 使用lambda表达式创建线程 Thread thread = new Thread(()

  • Java8新特性:lambda表达式总结

    一.Lambda 表达式的基础语法 Lambda 表达式的基础语法:Java8中引入了一个新的操作符 "->" 该操作符称为箭头操作符或 Lambda 操作符箭头操作符将 Lambda 表达式拆分成两部分: 左侧:Lambda 表达式的参数列表 右侧:Lambda 表达式中所需执行的功能,即 Lambda 体 语法格式一:无参数,无返回值 () -> System.out.println("Hello Lambda!"); 语法格式二:有一个参数,并且无

  • java8 多个list对象用lambda求差集操作

    业务场景:调用同步接口获取当前全部有效账户,数据库已存在部分账户信息,因此需要筛选同步接口中已存在本地的帐户. 调用接口获取的数据集合 List<AccountVo> list = response.getData().getItems(); 本地查询出来的账户集合 List<Account> towList = accountRepository.findAll(); 筛选差集代码 List<AccountVo> distinctByUniqueList = list

  • 简单易懂的java8新特性之lambda表达式知识总结

    一.概念 从本质上来说,它就是一个匿名函数,可以用来直接实现接口中的方法,从而简化代码.但是Lambda有一个限制,不能实现接口中的所有方法,所以Lambda表达式只能用于有且仅有一个必须需要实现的方法接口,这里需要注意必须需要实现这六个字. public interface Printer { //有一个需要实现的方法,可以使用Lambda表达式 void print(); } public interface Printer { //有一个需要实现的方法,可以使用Lambda表达式 void

  • JAVA8 lambda表达式权威教程

    Java 8新特性----Stream流 jdk8是Java 语言开发的一个主要版本,它支持函数式编程,新的 JavaScript 引擎,新的日期 API,新的Stream API 等等.今天就重点介绍一个非常重要得特性之一 lambda表达式 一:什么是 Stream? Stream(流)是一个来自数据源的元素队列并支持聚合操作 Java中的Stream并不会存储元素,而是按需计算. 数据源 流的来源. 可以是集合,数组,I/O channel, 产生器generator 等. 聚合操作 类似

  • 详解Java8中的lambda表达式、::符号和Optional类

    Java8中的lambda表达式.::符号和Optional类 0. 函数式编程 函数式编程(Functional Programming)属于编程范式(Programming Paradigm)中的用语,此外还有命令式编程(Imperative Programing)等,有兴趣的同学可以自行了解,我们这里大概解释一下函数式编程,在函数式编程中,输入一旦确定了,输出都确定了,函数调用的结果只依赖于传入的输入变量和内部逻辑,不依赖于外部,这样的写出的函数没有副作用.举个例子: public cla

  • Java8 Lambda和Invokedynamic详情

    目录 一.阐明lambda 二.匿名内部类 三.Lambdas和Invokedynamic 四.性能表现 一.阐明lambda Java8于2014年3月发布,并引入了lambda表达式作为其旗舰功能.我们可能已经在代码库中使用它们来编写更简洁.更灵活的代码.例如,我们可以将lambda表达式与新的Streams API结合起来,以表达丰富的数据处理查询: int total = invoices.stream() .filter(inv -> inv.getMonth() == Month.J

  • Java8 lambda表达式的10个实例讲解

    目录 例1.用lambda表达式实现Runnable 例2.使用Java 8 lambda表达式进行事件处理 例3.使用lambda表达式对列表进行迭代 例4.使用lambda表达式和函数式接口Predicate 例5.如何在lambda表达式中加入Predicate 例6.Java 8中使用lambda表达式的Map和Reduce示例 例7.通过过滤创建一个String列表 例8.对列表的每个元素应用函数 例9.复制不同的值,创建一个子列表 例10.计算集合元素的最大值.最小值.总和以及平均值

  • Java8 Lambda表达式详解及实例

    第一个Lambda表达式 在Lambda出现之前,如果我们需要写一个多线程可能需要下面这种方式: Runnable runnable = new Runnable() { @Override public void run() { System.out.println("Hello runnable"); } }; ... thread.start(); 上面的例子如果改成使用Lambda就会简单许多: Runnable noArgs = ()->System.out.print

  • Java8新特性:Lambda表达式之方法引用详解

    1.方法引用简述 方法引用是用来直接访问类或者实例的已经存在的方法或者构造方法.方法引用提供了一种引用而不执行方法的方式,它需要由兼容的函数式接口构成的目标类型上下文.计算时,方法引用会创建函数式接口的一个实例. 当Lambda表达式中只是执行一个方法调用时,不用Lambda表达式,直接通过方法引用的形式可读性更高一些.方法引用是一种更简洁易懂的Lambda表达式. Lambda表达式全文详情地址:http://blog.csdn.net/sun_promise/article/details/

  • java8中的lambda表达式简介

    目录 Lambda表达式的语法 Lambda表达式作用域 方法引用 指向静态方法的方法引用 指向任意类型实例方法的方法引用 指向现有对象的实例方法的方法引用 构造方法引用 lambda与匿名内部类 匿名内部类 总结 Lambda表达式类似匿名函数,简单地说,它是没有声明的方法,也即没有访问修饰符.返回值声明和方法名. Lambda允许把函数作为一个方法的参数(函数作为参数传递进方法中). Lambda表达式的语法 (parameters) -> expression 或 (parameters)

  • Java8新特性Lambda表达式的一些复杂用法总结

    简介 lambda表达式是JAVA8中提供的一种新的特性,它支持Java也能进行简单的"函数式编程". 它是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的lambda抽象(lambda abstraction),是一个匿名函数,即没有函数名的函数. 本文将介绍关于Java8 Lambda表达式的一些复杂用法,分享出来供大家参考学习,下面来一起看看详细的介绍: 复杂用法实例 传入数组ids,在list<Obj>上操作,找出Obj中id想匹配的,并且按

  • Java8中的lambda表达式入门教程

    1.基本介绍 lambda表达式,即带有参数的表达式,为了更清晰地理解lambda表达式,先上代码: 1.1 两种方式的对比 1.1.1 方式1-匿名内部类 class Student{ private String name; private Double score; public Student(String name, Double score) { this.name = name; this.score = score; } public String getName() { ret

随机推荐