【JVM】十七、线上系统中垃圾回收参数的精准调校指南
本文将更深入地探讨CMS垃圾回收过程中的一些细节问题,同时,我们还将讨论如何设置CMS常见的JVM参数。
1、前文回顾
在上一篇文章中,我们已经通过逐步的图解方式,详细解释了CMS垃圾回收的运行机制。简单来说,CMS垃圾回收器采用了四个阶段来进行垃圾回收,以尽量避免长时间的“Stop the World”现象。这四个阶段分别是:初始标记、并发标记、重新标记和并发清理。
在初始标记和重新标记这两个阶段,尽管会导致“Stop the World”,但它们的耗时非常短,因此对系统的影响并不大。
而并发标记和并发清理这两个阶段,虽然耗时较长,但它们可以与系统的其他工作线程并发运行,因此对系统的影响也相对较小。
这就是CMS垃圾回收器的基本原理。
然而,本文将更深入地探讨CMS垃圾回收过程中的一些细节问题,同时,我们还将讨论如何设置CMS常见的JVM参数。
2、并发垃圾回收引发的性能危机
首先大家回顾一下这个图。
CMS垃圾回收器在运行过程中,确实存在一个显著的问题。尽管它能够在进行垃圾回收的同时,让系统继续执行其他任务,但在并发标记和并发清理这两个最耗费时间的阶段,垃圾回收线程与系统工作线程的并行运行,可能会导致有限的CPU资源被垃圾回收线程占用一部分。
在并发标记阶段,需要对GC Roots进行深度追踪,以确定所有对象中哪些是仍然存活的。然而,由于老年代中存活的对象相对较多,这个过程需要追踪大量的对象,因此耗时较长。而在并发清理阶段,又需要将垃圾对象从各种随机的内存位置清理掉,这也是一个比较耗时的过程。
因此,在这两个阶段,CMS的垃圾回收线程会相对消耗较多的CPU资源。CMS默认启动的垃圾回收线程数量是(CPU核数 + 3)/ 4。我们以最常见的2核4G机器和4核8G机器为例,假设是2核CPU,本来CPU资源就有限,此时CMS还会有个“(2 + 3) / 4” = 1个垃圾回收线程,去占用宝贵的一个CPU核心。
所以,实际上,CMS这种并发垃圾回收机制的第一个问题就是可能会消耗CPU资源。
3、应对Concurrent Mode Failure的策略
第二个问题,是许多同学们都非常关心的一个话题。在并发清理阶段,CMS主要负责回收之前已经标记为垃圾的对象。然而,在这个过程中,系统仍在持续运行,这可能会导致一些对象在系统运行过程中进入老年代,并且同时被识别为垃圾对象。这类垃圾对象被称为“浮动垃圾”。
在并发清理阶段,CMS会尝试回收那些已经被标记为垃圾的对象。然而,由于系统仍在运行,有些对象可能会在这个阶段被移动到老年代,并且同时被识别为垃圾对象。这些对象被称为“浮动垃圾”,因为它们在系统运行过程中发生了变化,从而被识别为垃圾对象。

请参考上方图中红圈标记的部分,该部分展示了一个场景:在并发清理阶段,系统可能会先将一些对象分配到新生代。随后,可能触发了一次Minor GC,导致一部分对象被移动到老年代。如果在短时间内这些对象失去了引用,它们就成为了所谓的“浮动垃圾”。尽管这些对象已经变成了垃圾,但CMS垃圾回收器只能回收之前标记过的对象,而不会立即回收这些“浮动垃圾”,它们将在下一次GC时被处理。
为了确保在CMS垃圾回收过程中,仍有内存空间可供新对象进入老年代,通常会预留一部分空间。CMS垃圾回收的触发条件之一是当老年代的内存使用率达到设定的阈值时,将自动触发GC。通过参数“-XX:CMSInitiatingOccupancyFaction”,我们可以设置触发CMS垃圾回收的老年代内存使用率阈值。在JDK 1.6中,该参数的默认值是92%,即当老年代使用了92%的空间时,会自动触发CMS垃圾回收,保留8%的空间供并发回收期间新对象的分配。
然而,如果在CMS垃圾回收过程中,系统尝试将更多对象放入老年代,而这些对象的总大小超过了剩余的可用内存空间,会发生什么情况呢?这时,将发生所谓的“Concurrent Mode Failure”,即并发垃圾回收失败。在这种情况下,系统会暂停所有的应用线程(“Stop the World”),并切换到“Serial Old”垃圾回收器。这个过程将进行全面的GC Roots扫描,标记所有垃圾对象,禁止新对象的生成,然后将所有垃圾对象一次性回收。完成这些操作后,系统将恢复正常运行。
因此,在生产环境中,合理优化自动触发CMS垃圾回收的内存使用率阈值是非常重要的,以避免“Concurrent Mode Failure”的发生。在接下来的两篇文章中,我们将结合实际案例,深入分析垃圾回收参数的设置。
4、内存碎片问题解析
在之前的讨论中,我们已经提到了内存碎片问题。在Java的老年代垃圾回收器CMS中,它采用的是“标记-清理”算法。这种算法的工作原理是首先标记出需要回收的垃园对象,然后在一次垃圾回收过程中将这些对象清除。然而,这种方法会导致大量的内存碎片产生。
当内存碎片过多时,可能会导致新的对象无法在老年代找到足够的连续内存空间进行分配,从而触发全面的垃圾回收(Full GC)。因此,CMS并不是完全依赖“标记-清理”算法,因为过多的内存碎片实际上会引发更频繁的全面垃圾回收。
为了解决这个问题,CMS提供了一个参数:“-XX:+UseCMSCompactAtFullCollection”。这个参数在默认情况下是开启的,它的作用是在进行全面垃圾回收后,再次执行“停止世界”(Stop the World)操作,即暂停工作线程,然后进行内存碎片整理。具体来说,就是将存活的对象移动到一起,从而释放出大片的连续内存空间,减少内存碎片。
另一个参数是“-XX:CMSFullGCsBeforeCompaction”,它的含义是在执行多少次全面垃圾回收后,再进行一次内存碎片整理。这个参数的默认值是0,意味着每次全面垃圾回收后都会进行一次内存整理。如下图所示:

上图有一个画红圈的地方,就是说在垃圾回收之后,有一些内存碎片,接着会停止工作线程进行碎片整理,如下图:

大家可以看到,内存碎片整理完之后,存活对象都放在一起,然后空出来大片连续内存空间可供使用。