Java垃圾回收总是必须“停止世界”吗?

2022-09-02 19:46:20

我试图更深入地理解Java的垃圾回收。

在 HotSpot JVM 代际集合中,堆中有三个区域(年轻一代、老一代和永久一代)。此外,还有两种算法:

1)标记扫描压缩

2)并发标记和扫描

GC是否需要“停止世界”取决于它使用的算法而不是它操作的一代,这是真的吗?换句话说,如果我在所有三个领域使用1)作为GC算法,STW将始终发生?

另外,我理解不同之处在于第二个GC算法不需要压缩,这最终会导致碎片化。因此,第二个问题来了为什么压缩需要STW暂停?


答案 1

压缩导致STW暂停的关键原因如下,JVM需要移动对象并更新对它的引用。现在,如果您在更新引用之前移动对象,并且正在运行的应用程序从旧引用访问它,那么就会遇到麻烦。如果先更新引用,然后尝试移动对象,则更新的引用是错误的,直到对象被移动,并且在对象未移动时的任何访问都会导致问题。

对于CMS和并行收集器来说,年轻一代收集算法是相似的,它是停止世界,即当收集发生时应用程序停止 JVM正在做的事情是,标记所有可以从根集访问的对象,将对象从伊甸园移动到幸存者空间,并将收集中幸存下来的对象移动到旧一代。当然,JVM 必须更新对已移动对象的所有引用。

对于老一代并行收集器来说,在单个停止世界(STW)阶段中完成所有标记,压缩和参考更新,这导致以GB为单位的堆在几秒钟内暂停。对于具有严格响应时间要求的应用程序来说,这是痛苦的。到目前为止,Paralle收集器仍然是吞吐量或批处理方面最好的收集器(在Oracle Java中)。事实上,我们已经看到,对于相同的场景,即使暂停时间比CMS在并行收集器中花费的时间更多,我们仍然可以获得更高的吞吐量,我认为这与由于压缩而具有更好的空间局部性有关。

CMS通过同时进行标记解决了主要收集中高暂停的问题。有2个STW部分,初始标记(从根集获取引用)和备注暂停(标记结束时的一个小STW暂停,以处理标记和应用程序同时工作时对象图中的更改)。这两个暂停都在100 -200毫秒的范围内,对于几GB的堆大小和合理的应用程序线程数(记住更多的活动线程更多的根)

G1GC计划取代CMS并接受暂停的目标。通过增量压缩堆来处理碎片。虽然工作是渐进的,所以你可以得到更小的暂停,但这可能是以更频繁的暂停为代价的。

上述任何一种都不能在应用程序运行时压缩堆(CMS 根本不压缩)。AZUL GPGC 垃圾回收甚至可以在不停止应用程序的情况下进行压缩,还可以处理引用更新。因此,如果您想深入了解GC的工作原理,那么值得一读GPGC的算法。AZUL将其作为无暂停的收藏家进行销售。


答案 2

openjdk中所有免费提供的GC都有一些停止世界事件。不仅仅是指导性案例,其他因素(如去优化)也会触发安全点

但并非所有的停顿都是相等的。CMS 和 G1 不需要使用旧一代中的实时数据集来扩展其暂停时间,因为它们仅在暂停期间扫描对象的子集,并同时执行大部分工作,这与串行和吞吐量收集器不同。

ZGC(自 OpenJDK11 起可用)和 Shenandoah(自 12 日起可用)是收集器,它们进一步将暂停时间与实时数据集大小分离,并仅使用根集大小缩放其暂停。

此外,还存在其他避免全局暂停的GC实现 - 它们可能仍然会遇到每线程暂停 - 或者使暂停持续时间为O(1),即与实时数据集大小无关。一个经常被引用的例子是azul的C4收集器

因此,第二个问题来了为什么压缩需要STW暂停?

压缩意味着移动对象。移动对象意味着指针需要更新。当应用程序线程仍在运行时,安全地实现这一点非常困难或成本高昂。

并发算法通常会在吞吐量和复杂性方面付出一些成本,以换取较低的暂停时间。如果不执行压缩,CMS 对于并发收集器来说相对简单(!)。