垃圾回收器篇之二:CMS

CMS是一款低延迟的HotSpot虚拟机垃圾收集器,采用并发标记清除算法。它通过初始标记、并发标记、重新标记和并发清除四个阶段实现低停顿的垃圾回收。CMS使用记忆集和卡表来处理跨代引用,但可能导致内存碎片和浮动垃圾。当内存使用率达到阈值时,如默认的92%,CMS会启动。CMS的线程数计算涉及CPU核心数,其参数设置对性能有很大影响。CMS适用于对响应时间有高要求的应用,但可能会增加CPU使用率并可能导致Full GC。
摘要由CSDN通过智能技术生成

CMS(低延迟)

这款收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,他第一次实现了让垃圾收集线程与用户线程同时工作。
垃圾收集算法采用标记-清除算法(因为CMS强调低延迟,所以没有采用更耗费时间的标记-压缩算法),并且也会STW,

工作原理(Backgroud CMS 默认状态)

image.png

  1. 初始标记(Initial-Mark)阶段:在这个阶段中,程序中所有的工作线程都将会因为“Stop-the-World”机制而出现短暂的暂停,这个阶段的主要任务仅仅只是标记出GC Roots能直接关联到的对象。一旦标记完成之后就会恢复之前被暂停的所有应用线程。由于直接关联对象比较小,所以这里的速度非常快。
    1. JDK7及以前初始标记阶段默认是单线程,而JDK8以后默认是并发标记可通过-XX:+CMSParallellnitialMarkEnabled控制,+表示JDK8默认并行
  2. 并发标记(Concurrent-Mark)阶段:从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
    1. 重新标记前会进入预处理阶段
      标记阶段需要从GC Root开始对全堆进行可达性分析(包括年轻代),而为了减少扫描的年轻代对象的个数便对年轻代进行一次Young GC 所以,CMS有两个参数:
      CMSScheduleRemarkEdenSizeThreshold 默认值:2M CMSScheduleRemarkEdenPenetration 默认值:50%
      这两个参数组合起来就是Eden空间使用超过2M的时候启动可中断的并发预清理(CMS-concurrent-abortable-preclean),到Eden空间使用率到达50%的时候中断(但不是结束),进入Remark(重新标记阶段)。
    2. CMS提供了一个参数CMSMaxAbortablePrecleanTime ,默认为5S 只要到了5S,不管发没发生Minor GC,有没有到CMSScheduleRemardEdenPenetration都会中止此阶段,进入remark。
      另外CMS提供CMSScavengeBeforeRemark参数,使remark前强制进行一次Minor GC
  3. 重新标记(Remark)阶段:由于在并发标记阶段中,程序的工作线程会和垃圾收集线程同时运行或者交叉运行,因此为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录
  4. 并发清除(Concurrent-Sweep)阶段:此阶段清理删除掉标记阶段判断的已经死亡的对象,释放内存空间。由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的

由于最耗费时间的并发标记与并发清除阶段都不需要暂停工作,所以整体的回收是低停顿的。
另外,由于在垃圾收集阶段用户线程没有中断,所以在CMS回收过程中,还应该确保应用程序用户线程有足够的内存可用。因此,CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,而是当堆内存使用率达到某一阈值时,便开始进行回收,以确保应用程序在CMS工作过程中依然有足够的空间支持应用程序运行。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。

记忆集

记忆集是一种用于记录从非收集区域指向收集区域的指针集合的数据结构。

在跨代引用的场景中,当我们进行young gc时,我们的gc roots除了常见的栈引用、静态变量、常量、锁对象、class对象这些常见的之外,如果 老年代有对象引用了我们的新生代对象 ,那么老年代的对象也应该加入gc roots的范围中,但是如果每次进行young gc我们都需要扫描一次老年代的话,那我们进行垃圾回收的代价实在是太大了,因此我们引入了一种叫做记忆集的抽象数据结构来记录这种引用关系。

如果我们不考虑效率和成本问题,我们可以用一个数组存储所有有指针指向新生代的老年代对象。但是如果这样的话我们维护成本就很好,打个比方,假如所有的老年代对象都有指针指向了新生代,那么我们需要维护整个老年代大小的记忆集,毫无疑问这种方法是不可取的。因此我们引入了卡表的数据结构

卡表

记忆集是我们针对于跨代引用问题提出的思想,而卡表则是针对于该种思想的具体实现。(可以理解为记忆集是结构,卡表是实现类)
[1字节,00001000,1字节,1字节]
在hotspot虚拟机中,卡表是一个字节数组,数组的每一项对应着内存中的某一块连续地址的区域,如果该区域中有引用指向了待回收区域的对象,卡表数组对应的元素将被置为1,没有则置为0;
(1) 卡表是使用一个字节数组实现:CARD_TABLE[],每个元素对应着其标识的内存区域一块特定大小的内存块,称为"卡页"。hotSpot使用的卡页是2^9大小,即512字节
(2) 一个卡页中可包含多个对象,只要有一个对象的字段存在跨代指针,其对应的卡表的元素标识就变成1,表示该元素变脏,否则为0。GC时,只要筛选本收集区的卡表中变脏的元素加入GC Roots里。
卡表的使用图例

并发标记的时候,A对象发生了所在的引用发生了变化,所以A对象所在的块被标记为脏卡

继续往下到了重新标记阶段,修改对象的标记,同时清除脏卡标记。

Foregroud CMS:

其实这个也是CMS一种收集模式,但是他是并发失败才会走的模式。这里聊到一个概念,什么是并发失败呢?
并发失败官方的描述是:

如果 并发搜集器不能在年老代填满之前完成不可达(unreachable)对象的回收 ,或者 年老代中有效的空闲内存空间不能满足某一个内存的分配请求 ,此时应用会被暂停,并在此暂停期间开始垃圾回收,直到回收完成才会恢复应用程序。这种无法并发完成搜集的情况就成为 并发模式失败(concurrent mode failure) ,而且这种情况的发生也意味着我们需要调节并发搜集器的参数了。

简单来说,也就是我去进行并发标记的时候,内存不够了,这个时候我会进入STW,并且开始全局Full GC.
设置FullGC的开启

-XX:+UseCMSCompactAtFullCollection 表示开启FullGC

-XX:CMSFullGCsBeforeCompaction=0 表示多少次FullGC后采用MSC算法压缩堆内存,0表示每次FullGC后都会压缩,同时0也是默认值

那么什么时候会进行并发失败呢 换句话说,我们的难道非要满了之后才进行收集

-XX:CMSInitiatingOccupancyFraction
-XX:+UseCMSInitiatingOccupancyOnly

在 JVM 中,-XX:CMSInitiatingOccupancyFraction 是用于设置 CMS 回收器触发垃圾回收的阈值。当老年代空间使用率达到该阈值时,CMS 回收器将会被触发启动以进行垃圾回收。这个参数指定了老年代空间使用率的百分比,当达到这个阈值时,CMS 回收器将会启动。

而 -XX:+UseCMSInitiatingOccupancyOnly 参数是用来指定是否仅使用 CMSInitiatingOccupancyFraction 参数来触发 CMS 回收器的启动。如果设置了该参数,在 CMS 收集器初始化时将只使用 CMSInitiatingOccupancyFraction 参数来触发 CMS 回收器的启动,而不再考虑 CMS 收集器自身的启动阈值。

默认情况下,-XX:CMSInitiatingOccupancyFraction 的值是动态计算的,
公式为:((100 - MinHeapFreeRatio) + (double)( CMSTriggerRatio * MinHeapFreeRatio) / 100.0) / 100.0
默认值为: ((100 - 40) + (double) 80 * 40 / 100 ) / 100 = 92 %
即老年代空间使用率达到 92% 时将触发 CMS 回收器。(具体值会因jdk版本的不同而变化)
而 -XX:+UseCMSInitiatingOccupancyOnly 参数在默认情况下是未启用的,即不仅使用 CMSInitiatingOccupancyFraction 来触发 CMS 回收器的启动。

如何避免他的出现:
为了尽量避免并发模式失败发生,我们可以调节-XX:CMSInitiatingOccupancyFraction=参数,去控制当年老代的内存占用达到多少的时候(N%),便开启并发搜集器开始回收年老代。

CMS弊端

  1. 会产生内存碎片,导致并发清除后,用户线程可用的空间不足。在无法分配大对象的情况下,不得不提前触发Full GC。
  2. CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户停顿,但是会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。
  3. CMS收集器无法处理浮动垃圾。可能出现“Concurrent Mode Faillure"失败而导致另一次FullGC的产生。在并发标记阶段由于程序的工作线程和垃圾收集线程是同时运行或者交叉运行的,那么在并发标记阶段如果产生新的垃圾对象,CMS将无法对这些垃圾对象进行标记,最终会导致这些新产生的垃圾对象没有被及时回收,从而只能在下一次执行GC时释放这些之前未被回收的内存空间。

CMS参数设置

  • -XX:+UseConcMarkSweepGC手动指定使用CMS收集器执行内存回收任务。
    • 开启该参数后会自动将-XX:+UseParNewGC打开。即:ParNew(Young区用)+CMS(old区用)+Serial old的组合。
  • -XX:+CMSParallelRemarkEnabled采用并行标记方式降低停顿(默认开启)
  • -XX:+CMSConcurrentMTEnabled:被启用时,并发的CMS阶段将以多线程执行(因此,多个GC线程会与所有的应用程序线程并行工作)。(默认开启)
  • -XX:ConcGCThreads:定义并发CMS过程运行时的线程数。
  • **-XX:ParallelGCThreads:**定义CMS过程并行收集的线程数。
  • -XX:+CMSClassUnloadingEnabled:相对于并行收集器,CMS收集器默认不会对永久代进行垃圾回收。如果希望对永久代进行垃圾回收,可用设置-XX:+CMSClassUnloadingEnabled。默认关闭。
  • -X:CMSInitiatingOccupanyFraction设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收。
    • JDK5及以前版本的默认值为68,即当老年代的空间使用率达到68号时,会执行一次CMS回收。JDK6及以上版本默认值为92号
    • 如果内存增长缓慢,则可以设置一个稍大的值,大的阈值可以有效降低CMS的触发频率,减少老年代回收的次数可以较为明显地改善应用程序性能。反之,如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。因此通过该选项便可以有效降低Fū11GC的执行次数。
  • -Xx:CMSInitiatingoccupanyFraction设置堆内存使用率的阈值,一旦达到该阈值,便开始进行回收
    • JDK5及以前版本的默认值为68,即当老年代的空间使用率达到68%时,会执行一次CMS回收。JDK6及以上版本默认值为92%
    • 如果内存增长缓慢,则可以设置一个稍大的值,大的阈值可以有效降低CMS的触发频率,减少老年代回收的次数可以较为明显地改善应用程序性能。反之,如果应用程序内存使用率增长很快,则应该降低这个阈值,以避免频繁触发老年代串行收集器。因此通过该选项便可以有效降低Full GC的执行次数。

如果你想要最小化地使用内存和并行开销,请选Serial GC;
如果你想要最大化应用程序的吞吐量,请选Parallel GC;
如果你想要最小化GC的中断或停顿时间,请选CMS GC。

CMS的线程数计算公式

分别为以下两参数:

  • -XX:ParallelGCThreads=m // STW暂停时使用的GC线程数,一般用满CPU
    其中ParallelGCThreads 参数的默认值是:
    • CPU核心数 <= 8,则为 ParallelGCThreads=CPU核心数,比如4C8G取4,8C16G取8
    • CPU核心数 > 8,则为 ParallelGCThreads = CPU核心数 * 5/8 + 3 向下取整
    • 16核的情况下,ParallelGCThreads = 13
    • 32核的情况下,ParallelGCThreads = 23
    • 64核的情况下,ParallelGCThreads = 43
    • 72核的情况下,ParallelGCThreads = 48
  • **-XX:ConcGCThreads=n ** // GC线程和业务线程并发执行时使用的GC线程数,一般较小

ConcGCThreads的默认值则为:
ConcGCThreads = (ParallelGCThreads + 3)/4 向下取整。

  • ParallelGCThreads = 1~4时,ConcGCThreads = 1
  • ParallelGCThreads = 5~8时,ConcGCThreads = 2
  • ParallelGCThreads = 13~16时,ConcGCThreads = 4

CMS推荐配置参数:

第一种情况:8核16G左右服务器,再大的服务器可以上G1了 没必要

-Xmx12g -Xms12g
-XX:ParallelGCThreads=8
-XX:ConcGCThreads=2
-XX:+UseConcMarkSweepGC
-XX:+CMSClassUnloadingEnabled
-XX:+CMSIncrementalMode
-XX:+CMSScavengeBeforeRemark
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=70
-XX:CMSFullGCsBeforeCompaction=5
-XX:MaxGCPauseMillis=100  // 按业务情况来定
-XX:+ExplicitGCInvokesConcurrent
-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses
-XX:+PrintGCTimeStamps
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps

第二种情况:4核8G

-Xmx6g -Xms6g
-XX:ParallelGCThreads=4
-XX:ConcGCThreads=1
-XX:+UseConcMarkSweepGC
-XX:+CMSClassUnloadingEnabled
-XX:+CMSIncrementalMode
-XX:+CMSScavengeBeforeRemark
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=70
-XX:CMSFullGCsBeforeCompaction=5
-XX:MaxGCPauseMillis=100  // 按业务情况来定
-XX:+ExplicitGCInvokesConcurrent
-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses
-XX:+PrintGCTimeStamps
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps

第三种情况:2核4G,这种情况下,也不推荐使用,因为2C的情况下,线程上下文的开销比较大,性能可能还不如你不动的情况,没必要。非要用,给你个配置,你自己玩。

-Xmx3g -Xms3g
-XX:ParallelGCThreads=2
-XX:ConcGCThreads=1
-XX:+UseConcMarkSweepGC
-XX:+CMSClassUnloadingEnabled
-XX:+CMSIncrementalMode
-XX:+CMSScavengeBeforeRemark
-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=70
-XX:CMSFullGCsBeforeCompaction=5
-XX:MaxGCPauseMillis=100  // 按业务情况来定
-XX:+ExplicitGCInvokesConcurrent
-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses
-XX:+PrintGCTimeStamps
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
  • 17
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值