【问题标题】:Java's Serial garbage collector performing far better than other garbage collectors?Java 的串行垃圾收集器性能比其他垃圾收集器好得多?
【发布时间】:2012-04-02 02:14:14
【问题描述】:

我正在测试一个用 Java 编写的 API,它有望最大限度地减少处理通过网络接收的消息的延迟。为了实现这些目标,我正在尝试各种可用的垃圾收集器。

我正在尝试四种不同的技术,它们利用以下标志来控制垃圾收集:

1) 序列号:-XX:+UseSerialGC

2) 并行:-XX:+UseParallelOldGC

3) 并发:-XX:+UseConcMarkSweepGC

4) 并发/增量:-XX:+UseConcMarkSweepGC -XX:+CMSIncrementalMode -XX:+CMSIncrementalPacing

我在五个小时内运行了每项技术。我定期使用 ManagementFactory.getGarbageCollectorMXBeans() 提供的 GarbageCollectorMXBean 列表来检索收集垃圾所花费的总时间。

我的结果?请注意,此处的“延迟”是“我的应用程序 + API 用于处理从网络中提取的每条消息所花费的时间。”

串行:789 个 GC 事件,总计 1309 毫秒;平均延迟 47.45 us,中值延迟 8.704 us,最大延迟 1197 us

并行:1715 个 GC 事件,总计 122518 毫秒;平均延迟 450.8 us,中值延迟 8.448 us,最大延迟 8292 us

并发:4629 个 GC 事件,总计 116229 毫秒;平均延迟 707.2 us,中值延迟 9.216 us,最大延迟 9151 us

增量:5066 次 GC 事件,总计 200213 毫秒;平均延迟 515.9 us,中值延迟 9.472 us,最大延迟 14209 us

我发现这些结果太不可能了,以至于近乎荒谬。有谁知道我为什么会得到这些结果?

哦,为了记录,我使用的是 Java HotSpot(TM) 64 位服务器虚拟机。

【问题讨论】:

  • 您是否假设并行执行两件事必然比执行一件接一件的事情要快?
  • 我预计最大延迟会上升
  • 那么,在您的不同场景中,这 5 个小时内实际处理了多少条消息?您是在运行单线程还是多线程?
  • 我每次处理 4.318 亿条消息。该应用程序使用两个线程——一个在关键路径上从线路上抓取消息并将它们打包到一个队列中。非关键路径上的将它们从队列中取出并放入优先队列中;然后,每秒一次,它会排空优先级队列并计算那一秒的中值/平均/最大/等延迟统计信息。这台机器有两个 6 核 Intel Xeon 5680s 和 24 GB RAM。

标签: java garbage-collection


【解决方案1】:

我正在开发一个 Java 应用程序,该应用程序有望最大限度地提高吞吐量并最大限度地减少延迟

两个问题:

  • 这些通常是相互矛盾的目标,因此您需要确定每个目标对彼此的重要性(您是要牺牲 10% 的延迟来获得 20% 的吞吐量增益,反之亦然?您的目标是特定的延迟目标,超过这个目标是否更快并不重要?诸如此类。)
  • 您没有给出任何关于任何一个的结果

您所展示的只是在垃圾收集器中花费了多少时间。如果您实际上实现了更高的吞吐量,您可能期望看到更多的时间花在垃圾收集器上。或者换句话说,我可以对代码进行更改,以最大限度地减少您报告的值:

// Avoid generating any garbage
Thread.sleep(10000000);

你需要弄清楚什么对你来说实际上很重要。衡量所有重要的事情,然后找出权衡的地方。所以首先要做的是重新运行你的测试并测量延迟和吞吐量。您可能关心总 CPU 使用率(当然这与 GC 中的 CPU 不同),但是虽然您没有衡量您的主要目标,但您的结果并没有为您提供特别有用的信息.

【讨论】:

  • +1 很好的答案。我希望我可以为您的解决方案提供额外的 +1 以避免产生垃圾:-)
  • 三件事。首先,我明白目标往往是相互矛盾的。我想“延迟”将是我的主要目标。其次,我不只是遍历文件或其他东西。应用程序正在处理网络流量(应用程序的每次运行都使用相同的流量集),因此每次运行所处理的数据量都是相同的。第三,我稍后会在我的主帖中发布我的延迟结果。
  • 用户提出了一个公平的问题,有具体的数据,尽力而为。对不起,乔恩,对我来说,你不赞成,这个答案太笼统了,真的没有给用户和所有读者提供任何见解。
  • @Massimo:值得注意的是,延迟数据是在我回答问题后添加的。它最初只包括 GC 计时。我不是要你删除你的反对票,但我也不会费力地编辑一个 6 岁的答案。
【解决方案2】:

我一点也不觉得奇怪。

串行垃圾回收的问题在于,当它运行时,根本无法运行其他任何东西(也称为“停止世界”)。但这有一个好处:它将垃圾收集所花费的工作量保持在最低限度。

几乎任何类型的并行或并发垃圾回收都必须做大量额外的工作,以确保对堆的所有修改对于其余代码来说都是原子的。它必须停止只是那些依赖于特定更改的事情,然后停止足够长的时间来执行特定更改,而不是仅仅停止一段时间。然后它让该代码再次开始运行,到达下一个要进行更改的点,停止依赖它的其他代码片段,等等。

另一点(尽管在这种情况下,可能是一个相当小的问题)是当您处理更多数据时,您通常会产生更多垃圾,因此会花费更多时间进行垃圾收集。由于串行收集器在其工作时会停止所有其他处理,这不仅可以加快垃圾收集速度,还可以防止在此期间产生更多垃圾。

现在,为什么我说在这种情况下这可能是次要贡献者?这很简单:串行收集器在五个小时内只用了一秒多一点。尽管在这约 1.3 秒内没有做任何其他事情,但这只是 5 小时的一小部分,它可能对您的整体吞吐量没有太大(如果有的话)真正的影响。

总结:串行垃圾回收的问题不在于它总体上使用了过多的时间——而是如果它在您碰巧需要快速响应时立即停止世界,可能会非常不方便。同时,我应该补充一点,只要你的收集周期很短,这仍然是相当小的。理论上,其他形式的 GC 主要限制了最坏的情况,但实际上(例如,通过限制堆大小)您通常也可以使用串行收集器来限制最大延迟。

【讨论】:

    【解决方案3】:

    一位 Twitter 工程师在 2012 年 QCon Conference 上就这个话题进行了精彩的演讲 - 你可以观看它 here

    它讨论了 Hotspot JVM 内存和垃圾收集中的各种“世代”(Eden、Survivor、Old)。特别注意 ConcurrentMarkAndSweep 中的“并发”仅适用于老年代,即在一段时间内徘徊的对象。

    短期对象是“伊甸园”一代的 GCd - 这很便宜,但无论您选择哪种 GC 算法,都是“停止世界”的 GC 事件!

    建议首先调整年轻一代,例如分配许多新的伊甸园,以便对象有更多机会年轻时死去并以便宜的方式回收。使用 +PrintGCDetails, +PrintHeapAtGC, +PrintTenuringDistribution... 如果你得到超过 100% 的幸存者,那么就没有空间了,所以对象很快就会被提升为 Old - 这很糟糕。

    在为 Old 生成调优时,如果延迟是重中之重,建议先尝试 ParallelOld 自动调优(+AdaptiveSizePolicy 等),然后尝试 CMS,然后可能是新的 G1GC。

    【讨论】:

    • 如果上面的链接不适合您,也可以在 slideshare.net/aszegedi/… 上找到这些幻灯片。
    • 谢谢 - 我还更新了答案中的链接以指向视频的新位置。
    【解决方案4】:

    你不能说一个 GC 比另一个更好。这取决于您的要求和您的应用程序。

    但是如果你想最大化吞吐量并最小化延迟:GC 是你的敌人!您根本不应该调用 GC,并尝试阻止 JVM 调用 GC。

    使用串行并使用对象池。

    【讨论】:

      【解决方案5】:

      使用串行收集,一次只发生一件事。例如,即使有多个 CPU 可用,只有一个用于执行收集。当使用并行收集时,任务 垃圾收集被分成几部分,这些子部分同时执行,在不同的 CPU。同时操作使收集能够更快地完成,但代价是 一些额外的复杂性和潜在的碎片化。

      虽然串行 GC 仅使用一个线程来处理 GC,但并行 GC 使用多个线程来处理 GC,因此速度更快。当有足够的内存和大量的内核时,此 GC 很有用。它也被称为“吞吐量GC”。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2011-11-07
        • 2011-08-13
        • 2011-03-18
        • 2018-12-30
        • 1970-01-01
        • 2010-12-13
        • 2011-01-06
        • 2013-04-01
        相关资源
        最近更新 更多