在字节码级别理解 Java 8 个流

Understanding Java 8 Streams at the Bytecode Level

Java8 中有大量关于流的在线信息和教程。我发现的大部分内容都很好地解释了流的各种元素在概念层面上的工作方式。但是,我没有遇到太多描述 JVM 在底层如何实际实现和执行流的 material。

考虑比较 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());

如果您能深入了解这两种方法在字节码级别的底层差异,那就太好了。一些实际的字节码会更好。

随着时间的推移,答案已经增加了很多,所以我将从一个总结开始:

观察

  • 流 API 真正执行的跟踪乍一看看起来很可怕。许多调用和对象创建。但是请注意,对集合中的所有元素重复的唯一部分是 do-while 循环的主体。因此,除了一些恒定的开销之外,每个元素的开销是〜6个虚拟方法调用invokeinterface指令-我们的2个lambdas和4个accept()对接收器的调用) .
  • 提供给流 API 调用的 lambda 被转换为包含实现和 invokedynamic 指令的静态方法。它没有创建新对象,而是给出了如何在 运行 时间创建 lambda 的说明。之后在创建的 lambda 对象上调用 lambda 方法没有什么特别之处(invokeinterface 指令)。
  • 您可以观察流是如何延迟计算的。 filter()map() 将它们的操作包装在 StatelessOp 的匿名子 class 中,后者又扩展了 ReferencePipeline, AbstractPipeline and ultimately BaseStream。实际评估是在执行 collect().
  • 时完成的
  • 您可以看到流是如何真正使用 Spliterator 而不是 Iterator。注意许多分支检查 isParallel() - 并行分支将利用 Spliterator 的方法。
  • 创建了相当多的新对象,至少有 13 个。如果在循环中调用这样的代码,您可能 运行 陷入垃圾回收问题。对于单次执行应该没问题。
  • 我想查看两个版本的基准比较。 Streams 版本与“Java 7 版本”的差异可能会随着鱼数量的增加而变慢。另见 related SO question.

跟踪示例中流的执行情况

下面的伪代码通过使用流执行版本来捕获跟踪。请参阅此 post 的底部以了解如何读取跟踪。

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 */ }

感叹号指向 实际主力:fishListSpliterator 中的 do-while 循环。这是 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 :)

Streams API 在字节码级别使用 Lambdas

让我们看看执行代码的相关部分在字节码中是什么样子的。有趣的部分是如何

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

已翻译。您可以找到完整版 on 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[=13=](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 将创建匿名内部 class 实现 Predicate。这将被翻译成字节码:

new FishTest                        // create new instance of Predicate
dup
invokespecial FishTest.<init>()V    // call constructor

在 Java 8 中创建 lambda 被翻译为单个 invokedynamic 指令,无需创建新对象invokedynamic 指令的目的是将 lambda 的创建推迟到 运行 时间(与编译时间相反)。这会启用 caching lambda instances:

等功能

The use of invokedynamic lets us defer the selection of a translation strategy until run time. The runtime implementation is free to select a strategy dynamically to evaluate the lambda expression. ... The invokedynamic mechanics allow this to be done without the performance costs that this late binding approach might otherwise impose. ... For example, ... we generate the class the first time a given lambda factory site is called. Thereafter, future calls to that lambda factory site will re-use the class generated on the first call.

invokedynamic 的参数给出了构建相应功能接口实例的“配方”。它们代表 运行 时间实例创建的元工厂,引用它实现的方法(即 Predicate.test())和方法的实现。
在我们的例子中,实现是调用静态方法 boolean lambda$fish8[=41=](String) which the compiler sneaks in to our class. It contains the actual bytecode for f.contains("fish"). If you used lambda capturing method references (e.g. list::add), captured variables from the outer scope, etc., things would get more complex - look for occurences of "indy" in this document 以获取更多信息。

The other parts of bytecode 没那么有趣。 do-while 循环,除了明显的循环外,还包含一个 invokeinterface 指令,在相应的 Consumer 上调用 accept()accept() 调用沿接收器传播,沿途调用我们的 lambda。这里没有什么特别的,lambda 调用和通过接收器的传播都是简单的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;

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