lambdas for_Java 8 Lambdas-深入了解

lambdas for

Java 8于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的元空间(这是永久代的Java 8替代品)中的空间。 如果每个此类匿名内部类中的代码被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实例化类型AnonymousClassExample $ 1的对象。 同时将对新创建对象的引用推入堆栈。
  • 8:操作dup在堆栈上复制了该引用。
  • 10:然后,该值由invokeinspecial指令使用,该指令将初始化匿名内部类实例。
  • 13:堆栈的顶部现在仍然包含对该对象的引用,该引用使用putfield指令存储在AnonymousClassExample类的format字段中。

AnonymousClassExample $ 1是编译器为匿名内部类生成的名称。 如果您想放心,也可以检查AnonymousClassExample $ 1类文件,并找到用于实现Function接口的代码。

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

Lambda和invokedynamic

为了解决上一部分中解释的问题,Java语言和JVM工程师决定将转换策略的选择推迟到运行时。 Java 7引入的新的invokedynamic字节码指令为他们提供了一种有效实现这一目标的机制。 将lambda表达式转换为字节码的过程分为两个步骤:

  1. 生成一个invokedynamic调用站点(称为lambda factory ),该站点在被调用时返回将lambda转换为的Function Interface的实例;
  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;

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

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

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

实验室表现

这种方法的主要优点是性能特征。 仅仅将它们视为可简化为一个数字会很可爱,但是实际上这里涉及多个操作。

第一部分是链接步骤,它对应于上述lambda工厂步骤。 如果我们将性能与匿名内部类进行比较,则等效操作将是匿名内部类的类加载。 Oracle通过Sergey Kuksenko的权衡发表了性能分析 ,您可以看到Kuksenko在2013 JVM Language Summit 上发表了关于该主题的演讲 [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表达式的实现表现良好。 尽管匿名内部类需要手动优化以避免分配,但最常见的情况(不捕获其参数的lambda表达式)已由JVM为我们优化。

现场表现

当然,了解整体性能模型是一件好事,但是在实践中如何堆叠呢? 我们已经在一些软件项目中使用Java 8,总体上取得了积极的成果。 非捕获lambda的自动优化可以提供很好的好处。 确定了一个特定示例,该示例引发了有关未来优化方向的一些有趣问题。

有问题的示例是在处理某些代码时发生的,该代码可用于需要特别低GC暂停的系统中,理想情况下不需要。 因此,希望避免分配太多的对象。 该项目大量使用了lambda来实现回调处理程序。 不幸的是,我们仍然有很多回调,其中没有捕获任何局部变量,但是希望引用当前类的字段,甚至只是调用当前类的方法。 目前,这似乎仍需要分配。 这是一个代码示例,目的只是为了澄清我们在说什么:

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

与应用此方法的任何潜在优化一样,无论上下文如何,都有可能引入其他问题。

  1. 您纯粹出于性能原因而选择编写非惯用的代码。 因此存在可读性的折衷
  2. 分配权衡也很重要。 您正在向MessageProcessor添加一个字段,使其分配更大。 有问题的lambda的创建和捕获也减慢了对MessageProcessor的构造函数调用。

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

任何寻求优化其lambda表达式使用的其他最终用户也应采用这种方法。 尝试编写简洁,简单且实用的代码始终是最好的第一步。 诸如此吊装之类的任何优化措施都应仅针对真正的问题进行。 编写捕获捕获分配对象的lambda表达式并不是天生的坏事–就像编写调用`new Foo()`的Java代码的天生就好一样。

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

结论

在本文中,我们解释了lambda不仅是底层的匿名内部类,而且为什么匿名内部类不适合作为lambda表达式的合适实现方法。 通过lambda表达式实现方法,已经进行了很多工作。 目前,它们在大多数任务上都比匿名内部类要快,但是当前的状况还不是很完美。 仍然存在一些由测量驱动的手部优化的范围。

但是,Java 8中使用的方法不仅限于Java本身。 Scala历史上通过生成匿名内部类来实现其lambda表达式。 在Scala 2.12中,尽管已经开始使用Java 8中引入的lambda元工厂机制,但是随着时间的流逝,JVM上的其他语言也可能会采用这种机制。

翻译自: https://www.infoq.com/articles/Java-8-Lambdas-A-Peek-Under-the-Hood/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1

lambdas for

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值