Java的串行垃圾收集器比其他垃圾收集器表现更好吗?

12
我正在测试一个由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微秒,中位延迟8.704微秒,最大延迟1197微秒
并行:1715个GC事件,总计122518毫秒;平均延迟450.8微秒,中位延迟8.448微秒,最大延迟8292微秒
并发:4629个GC事件,总计116229毫秒;平均延迟707.2微秒,中位延迟9.216微秒,最大延迟9151微秒
增量:5066个GC事件,总计200213毫秒;平均延迟515.9微秒,中位延迟9.472微秒,最大延迟14209微秒
我认为这些结果几乎不可思议,甚至有些荒谬。有没有人知道我可能遇到这种结果的原因?
哦,对了,我正在使用Java HotSpot(TM) 64-Bit Server VM。

你是否认为并行执行两个任务一定比一个接一个地执行更快? - aioobe
我预计最大延迟会增加。 - jcoder
那么,在这五个小时内,您的不同场景实际处理了多少条消息?您是在运行单线程还是多线程? - pap
每次我处理了4.318亿条消息。该应用程序使用两个线程--一个在关键路径上从线路上抓取消息并将其打包到队列中。另一个在非关键路径上将它们从队列中取出并放入优先级队列中;然后,每秒钟一次,它会排空优先级队列并计算该秒的中位数/平均值/最大值等延迟统计数据。这台机器有两个6核Intel Xeon 5680和24 GB RAM。 - user1274193
5个回答

19
我正在开发一个Java应用程序,希望它能够最大化吞吐量并最小化延迟。这里有两个问题:
  • 这些目标通常是相互矛盾的,因此您需要确定每个目标相对于另一个目标的重要性(您是否愿意牺牲10%的延迟来获得20%的吞吐量增益或反之?您是否针对某个特定的延迟目标,超过该目标后,无论速度快慢都无关紧要?等等)
  • 您还没有提供任何与这些目标相关的结果。
您所展示的只是垃圾收集器中的时间。如果您实际上实现了更高的吞吐量,您可能预计在垃圾收集器中花费更多的时间。或者换句话说,我可以很容易地更改代码以最小化您报告的值:
// Avoid generating any garbage
Thread.sleep(10000000);

你需要弄清楚对你来说真正重要的是什么。测量所有重要因素,然后确定权衡的位置。所以首先要做的是重新运行测试并测量延迟和吞吐量。你可能还关心总 CPU 使用率(当然不同于 GC 中的 CPU),但如果你没有测量你的主要目标,那么你的结果就不会给你特别有用的信息。


1
+1 很棒的答案。我希望我能再多给一个 +1,以表扬你的解决方案避免了产生垃圾 :-) - aioobe
三件事情。首先,我知道目标经常是相互矛盾的。我想"延迟"应该是我的主要目标。其次,我不只是遍历一个文件之类的东西。这些应用正在处理网络流量(每次运行应用程序时使用相同的流量集),因此每次处理的数据量都是相同的。第三,我将在主帖中发布我的延迟结果。 - user1274193
用户提出了一个公正的问题,附带了具体数据,并尽力解释清楚。抱歉Jon,对我来说这是你的一次负评,因为你的回答太过笼统,没有给用户和所有读者任何深入的见解或指引。 - Massimo
@Massimo:值得注意的是,延迟数据是在我回答问题之后添加的。最初它只包括GC时间。我不要求您撤销您的负评,也不会费心编辑一个6年前的答案。 - Jon Skeet

4
我并不觉得这个结果令人惊讶。
串行垃圾回收的问题在于,当它运行时,其他任何东西都无法运行(也就是“停止世界”)。虽然这有一个好处:它将垃圾回收所花费的工作量减少到了最低限度。
几乎任何类型的并行或并发垃圾回收都需要做大量额外的工作,以确保对堆的所有修改对代码的其余部分都是原子的。它不再只是暂停一切一段时间,而是必须仅停止依赖于特定更改的那些内容,并且仅停止足够长的时间来执行该特定更改。然后它让该代码重新开始运行,到达下一个要进行更改的点,停止依赖它的其他代码,以此类推。
另一点(尽管在这种情况下可能是相当次要的)是,随着处理的数据越来越多,您通常希望生成更多的垃圾,因此花费更多时间进行垃圾回收。由于串行收集器在执行任务时会停止所有其他处理,这不仅使垃圾回收快速,而且还可以防止在此期间生成任何更多的垃圾。
那么,我为什么说这在这种情况下可能是一个次要的贡献者呢?原因很简单:串行收集器只用了五个小时中的一点多一秒。即使在那 ~1.3 秒内没有做其他事情,但它占用了五个小时的如此小的百分比,以至于它可能并没有对您的整体吞吐量产生任何实际影响。
总结:串行垃圾回收的问题不在于它总体使用过多的时间——而是如果它恰好在您需要快速响应的时候停止世界,那就非常不方便了。同时,我还应该补充说,只要您的收集周期很短,这仍然可以相当小。理论上,其他形式的 GC 主要限制您的最坏情况,但实际上(例如通过限制堆大小),您也经常可以使用串行收集器来限制最大延迟。

2
在2012年的QCon会议上,一位Twitter工程师发表了一次关于这个主题的精彩演讲 - 您可以在这里观看。
演讲讨论了Hotspot JVM内存和垃圾收集中的各种“代”(Eden、Survivor、Old)。特别要注意的是,“ConcurrentMarkAndSweep”中的“Concurrent”仅适用于老年代,即存在一段时间的对象。
短暂存在的对象从“Eden”代中进行垃圾回收 - 这很便宜,但无论您选择哪种GC算法,都是一个“停止世界”的GC事件!
建议首先调整年轻代,例如分配大量新的Eden,以便更有机会使对象早死并便宜地被回收。使用+PrintGCDetails、+PrintHeapAtGC、+PrintTenuringDistribution等命令。如果您获得了超过100%的survivor,则表示没有足够的空间,因此对象会很快晋升到老年代 - 这是不好的。
在为旧一代调整性能时,如果延迟是最重要的考虑因素,建议首先尝试使用ParallelOld与自动调整(例如AdaptiveSizePolicy等),然后尝试CMS,最后再尝试新的G1GC。

如果上面的链接无法使用,您也可以在http://www.slideshare.net/aszegedi/everything-i-ever-learned-about-jvm-performance-tuning-twitter找到幻灯片。 - ryenus
谢谢 - 我还更新了我的答案中的链接,指向视频的新位置。 - DNA

0

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

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


0

你不能说一个垃圾回收器比另一个更好。这取决于你的需求和应用程序。

但是,如果您想最大化吞吐量并最小化延迟:垃圾回收器是您的敌人!您不应该调用垃圾回收器,还应尽量防止JVM调用垃圾回收器。

选择串行并使用对象池。


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