理解Java 8流在字节码级别的实现

16

在Java 8中,关于流的信息和教程在网上有很多。大部分内容都能很好地解释流的不同元素在概念层面上是如何工作的。但我并没有找到过多少材料来描述流实际上是如何在JVM下实现和执行的。

考虑将对Collection的操作使用流和旧的Java 8之前的方式进行比较。这两种方法的底层字节码是否相同?性能是否相同?

为了使问题更具体化,考虑以下示例,我需要查找所有名称包含单词“fish”的鱼,并将每条匹配的鱼的首字母大写。(是的,我知道Hagfish实际上不是鱼,但我已经没有其他匹配的鱼名了。)

List<String> fishList = Arrays.asList("catfish", "hagfish", "salmon", "tuna", "blowfish");

// Pre Java-8 solution
List<String> hasFishList = new ArrayList<String>();

for (String fish : fishList) {
    if (fish.contains("fish")) {
        String fishCap = fish.substring(0, 1).toUpperCase() + fish.substring(1); 
        hasFishList.add(fishCap);
    }
}

// Java-8 solution using streams
List<String> hasFishList = fishList.stream()
    .filter(f -> f.contains("fish"))
    .map(f -> f.substring(0, 1).toUpperCase() + f.substring(1))
    .collect(Collectors.toList());

如果您能提供这两种方法在字节码级别下的不同之处的任何见解,那将非常好。 更好的是,提供一些实际的字节码。


8
流并不需要任何特殊的字节码处理。它们只是纯粹的Java API:类、方法、字段和其他API一样。而Lambda表达式需要编译器和虚拟机中的特殊处理。你可以使用javap命令查看字节码是什么。 - JB Nizet
2
Lambda可以理解为一个简单的new FunctionalInterface() { public X method(...) {...your lambda code...}}。实际情况更加复杂,但本质上并非如此--它只是为实际实现提供更多的灵活性。Tagir告诉你的与这个细节有关,与Streams API无关。 - Marko Topolnik
2
字节码不同是因为一个使用循环和条件语句,而另一个使用接受谓词、函数和收集器的 API,并使用管道。这两者在抽象级别上不同。但流并没有带来一组新的字节码指令。它们只是常规类。 - JB Nizet
3
所以,你询问的不是关于流(streams)的问题,而是关于 lambda 表达式的。基本上,lambda 表达式的主体被编译为一个私有方法,并且使用 invokedynamic 指令动态创建实现函数接口的类并调用私有方法。选择 invoke dynamic 是为了能够在不改变编译器生成的字节码的情况下实现更高效的功能。请参见 http://cr.openjdk.java.net/~briangoetz/lambda/lambda-translation.html 作为示例。搜索 invokedynamic、lambda 而不是 streams。 - JB Nizet
3
我的引用来源就在这里——那是你的话 :-) 但如果在真正的电脑上,我很乐意帮助你回答。 - Marko Topolnik
显示剩余7条评论
1个回答

27

答案内容如下:

随着时间的推移,答案已经增长了不少,因此我将从摘要开始:

观察

  • 流API实际执行的跟踪起始看起来很可怕。许多调用和对象创建。请注意,但是集合中所有元素的唯一重复部分是do-while循环的主体。因此,除了一些常量开销外,每个元素的开销约为6个虚拟方法调用(invokeinterface指令-我们的两个lambda和4个sinks上的accept()调用)。
  • 传递给流API调用的lambda被翻译为包含实现和一个invokedynamic指令的静态方法。它不是创建新对象,而是提供了运行时创建lambda的说明。在之后调用创建的lambda对象上的lambda方法没有什么特别的(invokeinterface指令)。
  • 您可以观察到流是如何懒惰地评估的。 filter()map()将其操作包装在继承自ReferencePipelineAbstractPipeline和最终BaseStream的匿名子类中。实际评估是在执行collect()时完成的。
  • 您可以看到流真正使用的是Spliterator而不是Iterator。请注意检查isParallel()的许多分支-并行分支将利用Spliterator的方法。
  • 有相当多的新对象被创建,至少13个。如果您在循环中调用此类代码,则可能会遇到垃圾收集问题。对于单次执行应该没问题。
  • 我想看到两个版本的基准比较。流版本可能会慢一些,差异从“Java 7版本”增加到增加的鱼的数量。还请参见相关的SO问题

跟踪流示例中流使用的执行过程

下面的伪代码记录了使用流的版本的执行跟踪。请参见本文底部的说明以了解如何阅读跟踪。

Stream stream1 = fishList.stream();
    // Collection#stream():
    Spliterator spliterator = fishList.spliterator();
        return Spliterators.spliterator(fishList.a, 0);
            return new ArraySpliterator(fishList, 0);
    return StreamSupport.stream(spliterator, false)
        return new ReferencePipeline.Head(spliterator, StreamOpFlag.fromCharacteristics(spliterator), false)
Predicate fishPredicate = /* new lambda f -> f.contains("fish") */
Stream stream2 = stream1.filter(fishPredicate);
    return new StatelessOp(this, StreamShape.REFERENCE, StreamOpFlag.NOT_SIZED) { /* ... */ }
Function fishFunction = /* new lambda f.substring(0, 1).toUpperCase() + f.substring(1) */
Stream stream3 = stream2.map(fishFunction);
    return new StatelessOp(this, StreamShape.REFERENCE, StreamOpFlag.NOT_SORTED | StreamOpFlag.NOT_DISTINCT) { /* ... */ }
Collector collector = Collectors.toList();
    Supplier supplier = /* new lambda */
    BiConsumer accumulator = /* new lambda */
    BinaryOperator combiner = /* new lambda */
    return new CollectorImpl<>(supplier, accumulator, combiner, CH_ID);
List hasFishList = stream3.collect(collector)
    // ReferencePipeline#StatelessOp#collect(Collector):
    List container;
    if (stream3.isParallel() && /* not executed */) { /* not executed */ }
    else {
    /*>*/TerminalOp terminalOp = ReduceOps.makeRef(collector)
            Supplier supplier = Objects.requireNonNull(collector).supplier();
            BiConsumer accumulator = collector.accumulator();
            BinaryOperator combiner = collector.combiner();
            return new ReduceOp(StreamShape.REFERENCE) { /* ... */ }
    /*>*/container = stream3.evaluate(terminalOp);
            // AbstractPipeline#evaluate(TerminalOp):
            if (linkedOrConsumed) { /* not executed */ }
            linkedOrConsumed = true;
            if (isParallel()) { /* not executed */ }
            else {
            /*>*/Spliterator spliterator2 = sourceSpliterator(terminalOp.getOpFlags())
                    // AbstractPipeline#sourceSpliterator(int):
                    if (sourceStage.sourceSpliterator != null) { /* not executed */ }
                    /* ... */
                    if (isParallel()) { /* not executed */ }
                    return spliterator;
            /*>*/terminalOp.evaluateSequential(stream3, spliterator2);
                    // ReduceOps#ReduceOp#evaluateSequential(PipelineHelper, Spliterator):
                    ReducingSink sink = terminalOp.makeSink()
                        return new ReducingSink()
                    Sink sink = terminalOp.wrapAndCopyInto(sink, spliterator)
                        Sink wrappedSink = wrapSink(sink)
                            // AbstractPipeline#wrapSink(Sink)
                            for (/* executed twice */) { p.opWrapSink(p.previousStage.combinedFlags, sink) }
                                return new Sink.ChainedReference(sink)
                        terminalOp.copyInto(wrappedSink, spliterator);
                            // AbstractPipeline#copyInto()
                            if (!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags())) {
                            /*>*/wrappedSink.begin(spliterator.getExactSizeIfKnown());
                            /*>*/ /* not important */
                            /*>*/supplier.get() // initializes ArrayList
                            /*>*/spliterator.forEachRemaining(wrappedSink)
                                    // Spliterators#ArraySpliterator#foreachRemaining(Consumer):
                                    // ... unimportant code
!!                                  do {
                                    /*>*/action.accept((String)a[i])
                                    } while (++i < hi) // for each fish :)
                            /*>*/wrappedSink.end() // no-op
                            } else { /* not executed */}
                        return sink;
                    return sink.get()
            }
    /*>*/if (collector.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH)) { return container; }
    /*>*/else { /* not executed */ }

感叹号指向真正的工作马: do-while循环fishListSpliterator中。以下是更详细的do-while循环追踪:

do {
/*>*/action.accept((String)a[i])
    if (predicate.test(u)) { downstream.accept(u); }  // predicate is our fishPredicate
        downstream.accept(mapper.apply(u)); // mapper is our fishFunction
            accumulator.accept(u)
                // calls add(u) on resulting ArrayList
} while (++i < hi) // for each fish :)

使用Lambda的流API字节码级别解析

让我们看一下执行代码的相关部分在字节码中的样子。有趣的部分是如何运用Lambda表达式。

fishList.stream().filter(f -> f.contains("fish")).map(f -> f.substring(0, 1).toUpperCase() + f.ubstring(1)).collect(Collectors.toList());

被翻译了。您可以在pastebin上找到完整版本。我将只专注于这里的filter(f -> f.contains("fish"))

invokedynamic #26,  0         // InvokeDynamic #0:test:()Ljava/util/function/Predicate; [
    java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    (Ljava/lang/Object;)Z, 
    FishTest.lambda$fish8$0(Ljava/lang/String;)Z, 
    (Ljava/lang/String;)Z
  ]
invokeinterface #27,  2       // InterfaceMethod java/util/stream/Stream.filter:(Ljava/util/function/Predicate;)Ljava/util/stream/Stream;
  

这里没有 与流 API 有关的具体内容,但使用了 新的 invokedynamic 指令 来创建 Lambda 表达式。Java 7 中 Lambda 的等价物是创建实现 Predicate 的匿名内部类。这将被转换为字节码:

new FishTest$1                        // create new instance of Predicate
dup
invokespecial FishTest$1.<init>()V    // call constructor
在Java 8中创建lambda被翻译为单个invokedynamic指令,而不需要创建新对象。 invokedynamic指令的目的是将lambda的创建推迟到运行时(而不是编译时)。这使得像缓存lambda实例之类的功能成为可能。
invokedynamic的参数给出了构造相应函数接口实例的“配方”。它们代表运行时实例创建的元工厂,实现其方法的引用(即Predicate.test())和方法的实现。在我们的情况下,实现是调用静态方法boolean lambda$fish8$0(String),编译器将其添加到我们的类中。它包含f.contains("fish")的实际字节码。如果您使用了捕获外部作用域的lambda引用(例如list::add)、捕获变量等,则事情会变得更加复杂-请查看此文档中的“indy”出现次数以获取更多信息。

字节码的其他部分则不太有趣。do-while循环除了明显的循环外,还包含一个调用Consumer上的accept()invokeinterface指令。 accept()调用沿着sinks传播,依次调用我们的lambda。没有什么特别的,lambda调用和通过sinks的传播都是简单的invokeinterface指令


如何阅读伪代码

缩进用于显示缩进代码上方展开的身体。以/*>*/开头的代码表示当前调用的延续(如果需要为了更好的可读性)。因此,调用

Objects.requireNonNull(new Object());

在跟踪伪代码中将被编写为:

Object o = new Object(); // extracted variable to improve visibility of new instance creation
Objects.requireNonNull(o);
    // this is the body of Objects.requireNonNull():
    if (o == null) {
    /*>*/throw new NullPointerException(); // this line is still part of  requireNonNull() body
    }
    return o;

我还跳过了一些不重要的调用,如空值检查、省略泛型参数,在适当的地方提取内联表达式到变量中等,以提高可读性。


不错的工作...顺便说一句,开始使用JMH要比这容易得多。 - Marko Topolnik
我以为JMH只是一个基准测试工具,它也能创建执行跟踪吗? - Mifeet
你在想性能方面的差异。与您在此处完成的工作相比,测量性能将少得多。 - Marko Topolnik
没错,回答的主要目标是分析引擎内部发生了什么。关于性能的评论只是我的附注 ;) - Mifeet
@Mifeet:关于这个问题,我有一个小注释:“这使得缓存lambda实例等功能成为可能。” 这段文字似乎讨论的是lambda类缓存,而不是lambda实例缓存。 - Lii

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接