在字节码级别了解 Java 8 流

2022-09-03 00:59:03

网上有大量关于 Java 8 中流的信息和教程。我发现的大部分内容都很好地解释了流的各种元素如何在概念层面上工作。但是,我还没有遇到太多描述流如何由引擎盖下的JVM实际实现和执行的材料。

考虑在使用流和以Java 8之前的老式方式执行操作之间比较一个操作。两种方法之间的底层字节码看起来是否相同?性能会一样吗?Collection

为了具体说明这一点,请考虑以下示例,其中我需要查找名称中包含单词“fish”的所有鱼,然后将每个匹配的鱼的第一个字母大写。(是的,我知道盲鳗不是真正的鱼,但我用完了匹配的鱼的名字。

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());

在字节码级别,您对这两种方法在引擎盖下可能有何不同有任何见解,都将是很棒的。一些实际的字节码会更好。


答案 1

随着时间的推移,答案已经增长了很多,所以我将从摘要开始:

观察

  • API真正执行的流跟踪乍一看看起来很可怕。许多调用和对象创建。但是,请注意,为集合中的所有元素重复的唯一部分是 do-while 循环的主体。因此,除了一些恒定的开销之外,每个元素的开销是~6个虚拟方法调用(指令 - 我们的2个lambda和4个对接收器的调用)。invokeinterfaceaccept()
  • 提供给流 API 调用的 lambda 将转换为包含实现和指令的静态方法。它不是创建新对象,而是给出了如何在运行时创建 lambda 的规定。之后在创建的 lambda 对象上调用 lambda 方法没有什么特别之处(指令)。invokedynamicinvokeinterface
  • 您可以观察如何懒惰地评估流。 并将他们的操作包装在匿名子类中,这些子类反过来扩展了RefleologicalPipeline,并最终扩展了BaseStream。实际评估在执行 时完成。filter()map()StatelessOpAbstractPipelinecollect()
  • 您可以看到流如何真正使用 Spliterator 而不是 .注意许多分叉检查 - 并行分支将利用 的方法。IteratorisParallel()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 */ }

感叹号指向实际的主力:s 中的 do-while 循环。以下是 do-while 循环的更详细的跟踪:fishListSpliterator

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

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

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的内容,但是新的调用动力学指令用于创建lambda。Java 7 相当于 lambdas 将创建匿名内部类来实现 。这将转换为字节码::Predicate

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

在 Java 8 中创建 lambda 被转换为单个指令,而无需创建新对象。指令的目的是将 lambda 的创建推迟到运行时(而不是编译时)。这将启用缓存 lambda 实例等功能:invokedynamicinvokedynamic

使用 invokedynamic 可以让我们将转换策略的选择推迟到运行时。运行时实现可以自由动态选择策略来评估 lambda 表达式。...调用动力学机制允许在没有这种后期绑定方法可能带来的性能成本的情况下完成此操作。...例如。。。我们在第一次调用给定的 lambda 工厂站点时生成类。此后,对该 lambda 工厂站点的未来调用将重用第一次调用时生成的类。

给出用于构造相应功能接口的实例的“配方”的参数。它们表示运行时实例创建的元工厂,引用它实现的方法(即)和方法的实现。
在我们的例子中,实现是调用静态方法布尔lambda$fish8$0(String),编译器将其潜入到我们的类中。它包含 的实际字节码。如果您使用 lambda 捕获方法引用(例如),从外部范围捕获的变量等,事情将变得更加复杂 - 在本文档中查找“indy”的出现以获取更多信息。invokedynamicPredicate.test()f.contains("fish")list::add

字节码的其他部分不太有趣。除了明显的循环之外,do-while 循环还包含一条调用相应 . 调用沿接收器向下传播,沿途调用我们的 lambda。这里没有什么特别的,lambda调用和通过接收器的传播都是简单的调用接口指令invokeinterfaceaccept()Consumeraccept()


如何读取伪代码

缩进用于在缩进代码上方显示展开的调用正文。以 /*>*/ 开头的代码表示当前调用的延续(如果需要以提高可读性)。因此,调用

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;

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


答案 2

推荐