从 GC 的角度来看 Java 流的影响或由 GC 处理短期对象

aka*_*lou 7 java performance garbage-collection premature-optimization java-stream

网上有一些文章提到了使用Stream-s 而不是旧的loop-s 的一些缺点:

But is there any impact from the GC perspective? As I assume (is it correct?) that every stream call creates some short-lived objects underneath. If the particular code fragment which uses streams is called frequently by the underlying system could it cause eventually some performance issue from the GC perspective or put extra pressure on GC? Or the impact is minimal and could be ignored most of the time?

Are there any articles covering this more in detail?

Eug*_*ene 5

公平地说,当霍尔格已经通过他的答案将主要思想联系起来时,给出答案是非常复杂的;我仍然会尝试。

GC 上的额外压力- 可能是。执行 GC 周期需要额外的时间 - 很可能不会。可以忽略吗?我想说完全可以。最后,您关心的是GC- 只需很少的时间即可回收大量空间,最好是通过超小型的世界静止事件。

让我们谈谈 GC 主要两个阶段的潜在开销:标记和疏散/重新分配 (Shenandoah/ZGC)。第一mark阶段,GC找出什么是垃圾(通过实际识别什么是活的)。

如果 Stream 内部创建的对象不可访问,则永远不会扫描它们(此处为零开销),如果可访问,则扫描它们将非常快。故事的另一面是:当您创建一个对象并可能在标记阶段运行GC触摸它时,LoadBarrier 的慢速路径(在 的情况下)将处于活动状态。我认为这将增加该特定阶段的总时间以及队列中的一些空间。Aleksey Shipilev 在一次演讲中表示,他试图测量执行单个屏障的开销,但未能成功,因此他进行了测量,时间约为数十。我不知道 ZGC 的具体细节,但 LoadBarrier 也在那里。ShenandoahnsGCSATB3ns

要点是,这个标记阶段是在应用程序运行时以并发方式完成的,因此您的应用程序仍然可以完美运行。即使某些 GC 代码会被触发去做一些特定的工作(负载屏障),它也会非常快并且对您来说完全透明。

第二阶段是“压缩”,即为未来的分配腾出空间。GC 的作用是将活动对象从垃圾最多的区域(Shenandoah当然)移动到空的区域。但仅限于有生命的物体。因此,如果某个区域有 100 个对象,并且只有 1 个对象处于活动状态,则只有 1 个对象会被移动,那么整个区域将被标记为空闲。因此,如果 Stream 实现仅生成垃圾(即:当前不存在),那么它对于 GC 来说是“免费午餐”,它甚至不知道它的存在。

这里更好的情况是这个阶段仍然是同时完成的。为了保持“并发”活跃,您需要知道 GC 周期从开始到结束分配了多少内存。这个数量是为了让 GC 满意而需要在 java 进程之上拥有的最小“额外”空间。

总的来说,你看到的是一个非常微小的影响;如果有的话。

  • “死对象是自由的”论点的问题在于,虽然它在技术上是正确的,但它忽略了故事的另一部分:分配迫使 GC 采取行动,这使得它们更频繁地处理活对象。因此,在完全年轻的工作负载中,GC 成本确实很小,但是一旦堆中有实时数据,分配就会在很大程度上迫使 GC 进行操作。 (3认同)
  • 好吧,我“无法测量单个障碍”,因为构建一个在热路径上仅执行一个障碍的程序相当困难。是的,单个屏障*fastpath*只需要几个CPU周期,甚至可以隐藏在流水线中。但这仍然是成本,并且可以在整个应用程序中进行衡量,通常可能会导致吞吐量下降个位数的百分比。 (2认同)
  • 这是一个令人愉快的想法:在凭经验看到问题之前担心 GC 是愚蠢的行为。GC 管理的代码的性能模型非常复杂,您很可能处于 GC 成本可以忽略的阶段,也可能处于 GC 成本成为破坏性因素的阶段。仅当您了解有关应用程序和所使用的 GC 的许多确切信息时,根据第一原则争论您最终会得到什么结果才有效。 (2认同)