关于java:深入解析CMS垃圾回收器

14次阅读

共计 6081 个字符,预计需要花费 16 分钟才能阅读完成。

本文已收录至 GitHub,举荐浏览 👉 Java 随想录

微信公众号:Java 随想录

原创不易,重视版权。转载请注明原作者和原文链接

后面几篇文章都在介绍 GC 的工作原理,上面开始大家期待的垃圾回收器章节。一共有三篇:CMS、G1 和 ZGC。

本篇文章先来介绍 CMS。

纵观全书《深刻了解 JVM 虚拟机》第三版,在垃圾回收器这一篇章,对于 CMS 的笔墨是十分多的。

CMS 也是 JVM 面试的一个重点,只有说起垃圾回收器,CMS 能够说不得不问,聊好了,会让面试官感觉你有两把刷子。

话不多说,间接进入正题。

CMS 简介

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收进展工夫为指标的收集器。

CMS 启用参数:-XX:+UseConMarkSweepGCCMS 是老年代垃圾收集器,应用的是标记 - 革除算法。

在 CMS 之前的垃圾回收器,要么就是串行垃圾回收形式,要么就是关注零碎吞吐量,而 CMS 垃圾回收器的呈现,则突破了这个难堪的场面。

CMS 收集器是 HotSpot 虚拟机谋求低进展的第一次胜利尝试,其开启了 GC 回收器关注 GC 进展工夫的历史。

CMS 垃圾回收器之所以可能实现对 GC 进展工夫的管制,其要害是「三色标记算法」(不理解的同学去翻我之前写的文章)。

通过三色标记算法,实现了垃圾回收线程与用户线程并发执行,从而极大地升高了零碎响应工夫。

如果在 JDK9 之后应用 CMS 垃圾收集器后,默认年老代就为 ParNew 收集器,并且不可更改,同时 JDK9 之后被标记为不举荐应用,JDK14 就被删除了。

能够说 CMS 是垃圾回收器的一个里程碑。

运作过程

CMS 整个运作过程分为四个大阶段,包含:

  1. 初始标记(CMS initial mark)
  2. 并发标记(CMS concurrent mark)
  3. 从新标记(CMS remark)
  4. 并发革除(CMS concurrent sweep)

留神:这里说的是四个大阶段,两头还会有其余的过渡小阶段,然而最次要的工夫损耗在这四个阶段上。

其中「初始标记」、「从新标记」这两个步骤依然须要 Stop The World,这点是须要留神的。

CMS 在各个阶段都做了哪些事呢,别着急,听我缓缓道来。

初始标记

这一步依然须要暂停所有的其余线程,但这个阶段会很快实现。它的目标是「标记所有的根对象,以及被根对象间接援用的对象,以及年老代指向老年代的对象」。

也就是说初始标记阶段,只会标记第一层,不会向下追溯去深度遍历。

并发标记

在此阶段中,垃圾回收器将遍历对象图,从 GC Roots「向下追溯 」,标记所有可达的对象。 这个过程是四个阶段中耗时最长的,然而不须要进展用户线程,能够与垃圾收集线程一起并发运行。

在此阶段,利用线程与垃圾回收线程是并发运行的。如果利用线程产生了新的对象,并且批改了老年代中的对象援用,那么这些变动可能被并发进行的垃圾回收线程疏忽掉,这就可能造成「漏标」问题,即有些本该被标记的对象没有被标记。

为了解决这个问题,CMS 采纳了卡表。

当利用线程试图批改老年代的某个对象援用时,把这些发生变化的对象所在的 Card 标识为 Dirty,这样后续就只须要扫描这些 Dirty Card 的对象,从而防止扫描整个老年代。

对于卡表,之前在讲跨代援用的时候介绍过,遗记的同学去翻翻我之前写的文章。

并发预处理

并发预处理能够通过参数:-XX:-CMSPrecleaningEnabled管制,默认开启。

并发预处理阶段用户线程能够与垃圾回收线程一起执行。

并发预处理目标在于心愿能尽可能减少下一个阶段「从新标记」所耗费的工夫,因为下一个阶段从新标记是须要 Stop The World 的。

在前个并发阶段中,老年代的对象援用关系可能会发生变化,所以并发预处理这个阶段会扫描可能因为并发标记时导致老年代发生变化的对象,会再扫描一遍标记为 Dirty 的卡页,并标记被 Dirty 对象间接或间接援用的对象,而后革除 Card 标识。

可勾销的并发预处理

此阶段也不进行应用程序,本阶段尝试在 STW 的最终标记阶段之前尽可能多做一些工作。本阶段的具体工夫取决于多种因素,因为它循环做同样的事件,直到满足某个退出条件。

在该阶段,次要循环去做两件事:

  1. 解决 From 和 To 区的对象,标记可达的老年代对象。
  2. 和上一个阶段一样,扫描解决 Dirty Card 中的对象。

在预处理步骤后,如果满足上面这个条件,就会开启可中断的预处理:

  • Eden 的应用空间大于-XX:CMSScheduleRemarkEdenSizeThreshold,这个参数的默认值是 2M,如果新生代的对象太少,就没有必要执行该阶段,间接执行从新标记阶段。

如果满足上面的条件,就会退出循环:

  • 设置了 CMSMaxAbortablePrecleanLoops 循环次数,并且执行的次数大于或者等于这个值的时候,默认为 0
  • CMSMaxAbortablePrecleanTime,执行可中断预清理的工夫超过了这个值,这个参数的 默认值是 5000 毫秒
  • Eden 的使用率达到 -XX:CMSScheduleRemarkEdenPenetration 这个参数的默认值是 50%

如果在可勾销的并发预处理可能产生一次 Minor GC,那样可能加重从新标记阶段的工作。

如果始终没等到 Minor GC,这个时候进行从新标记的话,可能会产生间断进展,假如新生代在从新标记的时候产生了 Minor GC(STW),从新标记又是 STW 的,因而可能会产生间断进展。

CMS 提供了参数CMSScavengeBeforeRemark,使从新标记前强制进行一次 Minor GC。

这个参数有利有弊,利是升高了 Remark 阶段的进展工夫,弊的是在新生代对象很少的状况下也多了一次 YGC,哪怕在可勾销的并发预处理阶段曾经产生了一次 YGC,而后在该阶段又会去傻傻的触发一次。

从新标记

在从新标记(Remark)阶段,实际上是要扫描整个堆内存的,包含新生代和老年代

这是因为在并发标记阶段,应用程序线程还在运行,可能会有新对象被调配到新生代,并且可能会有援用关系的扭转。如果不扫描新生代,就可能会漏掉一些被援用的对象,导致误删。

然而实际上,因为各种优化技术,比方增量更新(Incremental Update)和卡表(Card Table),从新标记阶段能够只扫描局部区域。例如,只须要扫描在并发标记阶段中被批改过的那局部堆内存区域,而无需全盘扫描整个堆内存。

上述对象中可能有一些曾经在「并发预处理 」阶段和「 可勾销的并发预处理」阶段被解决过,但总存在没来得及解决的。

这里有个小细节,其实从新标记也是能够并发执行的。

能够通过-XX:ParallelRemarkEnabled,参数启用并行从新标记,当设置为 true 时,它容许在从新标记阶段应用多线程。

请留神,这个选项不影响初始标记阶段,那个阶段仍将应用单线程执行。

启用 -XX:ParallelRemarkEnabled 参数并行执行 CMS 的从新标记阶段能够缩小垃圾回收时利用的进行工夫,但也有可能带来一些毛病:

  1. 资源耗费:并行执行须要更多的 CPU 资源,如果零碎上运行着其余须要 CPU 的工作,这可能会升高它们的性能。
  2. 复杂性减少:并行化解决通常减少了零碎的复杂性,可能会导致更难预测和调试的性能问题。
  3. 不稳定性:只管并行从新标记通常能够提高效率,但在某些特定硬件和工作负载下,可能会失去相同的后果。

因而,是否应用 -XX:ParallelRemarkEnabled 取决于具体的利用和硬件环境。在开启这个选项之前,最好先在仿真环境中进行充沛的测试,以评估它对性能的影响。

并发革除

最初是并发革除阶段,在此阶段中,垃圾回收器删除未被标记的对象,并回收他们占用的内存空间,同样,该步骤也是与利用线程并发执行的。

这个过程,还是有可能用户线程在一直产生垃圾,但只能留到下一次 GC 进行解决了,产生的这些垃圾被叫做「浮动垃圾」。

另外,CMS 应用「闲暇列表(free-list)」,在并发革除阶段完结后,CMS 会将未被标记的内存(即垃圾对象占据的内存)收集起来,组成一个闲暇列表。

这个闲暇列表保留了可用于新对象调配的内存块信息。当须要调配新对象时,JVM 能够间接从闲暇列表中找到适合大小的内存块进行调配,而无需进行残缺的垃圾回收。

然而,这种办法也有其毛病,例如可能会导致内存碎片化问题。如果间断的闲暇内存块不足以满足新的内存申请,就须要触发一次齐全的垃圾收集,此时则可能会引起较长时间的暂停。

这些阶段都走完了当前会重置 CMS 算法相干的外部数据,为下一次 GC 循环做筹备

因为在整个过程中耗时最长的并发标记和并发革除阶段中,垃圾收集器线程都能够与用户线程一起工作,所以从总体上来说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。

CMS 的缺点

CMS 并不完满,存在一些毛病。

处理器资源敏感

CMS 收集器是比拟耗费 CPU 资源的,对处理器资源是比拟敏感的。

在并发阶段,它不会导致用户线程进展,但会占用一部分线程(或者说处理器的计算能力)来进行垃圾回收,从而导致应用程序变慢,升高总吞吐量。

低提早和高吞吐,往往无奈同时达成,低提早有时是就义高吞吐换得的,有得必有失。

CMS 默认启动的回收线程数是(处理器外围数量 +3)/4,也就是说,如果处理器外围数在四个或以上,并发回收时垃圾收集线程只占用不超过 25% 的处理器运算资源。

然而当处理器外围数量有余四个时,CMS 对用户程序的影响就可能变得很大。如果利用原本的处理器负载就很高,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度突然大幅升高。

CMS 回收线程数量能够通过 -XX:ParallelCMSThreads=<N> 这个 JVM 参数来设定,其中 <N> 代表冀望的线程数。

请留神,这个参数只影响 CMS 中进行并发标记和革除的线程数量,并不影响其余局部(如初始标记和从新标记)的线程数量。

为了缓解这种状况,虚拟机还提供了一种称为「增量式并发收集器”(Incremental Concurrent Mark Sweep/i-CMS)」的 CMS 收集器变种。

增量式并发收集器 在并发标记、清理的时候让收集器线程、用户线程交替运行,尽量减少垃圾收集线程的独占资源的工夫,这样整个垃圾收集的过程会更长,但对用户程序的影响就会显得较少一些。

直观感触是速度变慢的工夫更多了,但速度降落幅度就没有那么显著。

实践证明增量式的 CMS 收集器成果很个别,从 JDK 7 开始,i-CMS 模式曾经被申明为「deprecated」,即已过期不再提倡用户应用,到 JDK 9 公布后 i -CMS 模式被齐全废除。

无奈解决“浮动垃圾”

在 CMS 的并发标记和并发清理阶段,用户线程是还在持续运行的,程序在运行天然就还会随同有新的垃圾对象一直产生。

但这一部分垃圾对象是呈现在标记过程完结当前,CMS 无奈在当次收集中解决掉它们,只好留待下一次垃圾收集时再清理掉。这一部分垃圾就称为「浮动垃圾」。

就好比你妈一边在清扫房间,你一边在丢纸屑,房间永远也清扫不完。

因为在垃圾收集阶段用户线程还须要继续运行,那就还须要预留足够内存空间提供给用户线程应用,因而 CMS 收集器不能像其余收集器那样期待到老年代简直齐全被填满了再进行收集。

在 JDK5 的默认设置下,CMS 收集器当老年代应用了 68% 的空间后就会被激活。到了 JDK 6 时,CMS 收集器的启动阈值就曾经默认晋升至 92%,咱们能够通过 -XX:CMSInitiatingOccupancyFraction 参数自行调节。

但这又会更容易面临另一种危险:要是 CMS 运行期间预留的内存无奈满足程序调配新对象的须要,就会呈现一次「并发失败(Concurrent Mode Failure)」。

这时候虚拟机将不得不启动后备预案:解冻用户线程的执行,长期启用 Serial Old 收集器来从新进行老年代的垃圾收集,但这样进展工夫就很长了。

顺嘴提一句:Serial Old 应用的是「标记 - 整顿 ”(Mark-Compact)」算法。

总结:CMS 收集器无奈解决「浮动垃圾(Floating Garbage)」,甚至有可能呈现「Con-current Mode Failure」失败进而导致另一次齐全 Stop The World 的 Full GC 的产生。

内存碎片

CMS 是一款基于「标记 - 革除 」算法实现的收集器,在垃圾收集算法的时候咱们说过,标记 - 革除会产生「 内存碎片」。

空间碎片过多时,将会给大对象调配带来很大麻烦,往往会呈现老年代还有很多残余空间,但就是无奈找到足够大的间断空间来调配以后对象,而不得不提前触发一次 Full GC 的状况。

为了解决这个问题,CMS 收集器提供了一个 -XX:+UseCMS-CompactAtFullCollection 开关参数(默认是开启的,此参数从 JDK 9 开始废除)。

用于在 CMS 收集器不得不进行 Full GC 时开启内存碎片的合并整顿过程,然而整顿过程又必须挪动存活对象

这样空间碎片问题是解决了,但进展工夫又会变长,属于是白忙活了。

因而虚拟机设计者们还提供了另外一个参数-XX:CMSFullGCsBefore-Compaction(此参数从 JDK 9 开始废除)。

这个参数的作用是要求 CMS 收集器在执行过若干次(数量由参数值决定)不整顿空间的 Full GC 之后,下一次进入 Full GC 前会先进行碎片整顿(默认值为 0,示意每次进入 Full GC 时都进行碎片整顿)。

须要留神的是,尽管内存压缩能够缩小内存碎片,进步内存利用效率,但同时也会减少 GC 的暂停工夫,因而可能会对利用的响应性能产生负面影响。因而,在调整此参数时,须要思考利用的个性和需要,进行适当的衡量。

总结

最初让咱们对 CMS 做个总结:

CMS 垃圾收集器在 Java 的垃圾回收历史上占据了重要的位置。它的呈现为解决低暂停工夫,高响应性的零碎提供了一种新的可能。

能够说,CMS 是那种将「用户体验」放在第一位的角色,它谋求晦涩的用户交互,防止因为垃圾收集导致的长时间进展。

正如一个重视社交技巧,长于营造轻松氛围的人,CMS 的长处在于它的并发解决能力,把大部分工作在线程间平滑解决,使得应用程序能够和垃圾收集同时进行,尽量减少了忽然的进展。这就像是在一场团聚中,「轻松愉快」是 CMS 的拿手好戏。

然而,每个人都有本人的短板,CMS 也不例外。因为 CMS 为了升高暂停工夫而不执行内存整理,所以在继续运行一段时间后,可能会产生很多内存碎片,影响零碎的性能体现。

此外,它在并发清理时须要更多的 CPU 资源,这就像一个社交达人可能须要付出更多的工夫和精力来解决人际关系一样。

总的来说,CMS 的呈现极大地推动了低提早利用的倒退,标记着垃圾收集器从单纯的内存治理进化到更加重视用户体验的阶段。只管它也有一些问题,但没有人是完满的,咱们都在一直自我改良中后退,CMS 也一样。

本篇文章到这完结咯,好好消化下,感觉有播种点个赞,下篇文章持续卷。


感激浏览,如果本篇文章有任何谬误和倡议,欢送给我留言斧正。

老铁们,关注我的微信公众号「Java 随想录」,专一分享 Java 技术干货,文章继续更新,能够关注公众号第一工夫浏览。

一起交流学习,期待与你共同进步!

正文完
 0