https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html#concurrent_mark_sweep_cms_collectorhttps://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html#concurrent_mark_sweep_cms_collector
https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/cms.html#concurrent_mark_sweep_cms_collector
CMS回收流程
Concurrent Mark Sweep收集器是一种以获取最短回收停顿时间为目标的垃圾收集器。
采用标记-清除算法,整个过程分为4步
- 初始化标记 CMS initial mark,标记GC Root直接关联对象,不用Tracing,速度很快。
- 并发标记 CMS concurrent mark,进行GC Root Tracing。
- 重新标记 CMS remark,修改并发标记因用户程序变动的内容。
- 并发清除 CMS concurrent sweep,清除不可达对象回收空间,同时有新垃圾产生,留着下次清理,称为浮动垃圾。
由于整个过程中,并发标记和并发清除时,收集器线程和用户线程同时进行,所以总体来说,CMS收集器的内存回收过程时与用户线程一起并发执行的。
优点:并发收集、低停顿。
缺点:产生大量空间碎片、并发阶段会降低吞吐量。
两种模式和一种压缩策略
Background CMS模式
实际上cms有另外两个阶段,这里可以将并发标记分为并发预处理和可中止预处理
- 初始化标记
- 并发标记
- 并发预处理
- 可中止预处理
- 重新标记
- 并发标记
这里涉及到几个问题
当进行老年代回收时,我们如何判断被年轻代指向的老年代对象是存活对象?如何快速定位?
如果是扫描新生代肯定会很慢,随之而来的问题就是停顿时间会变长,但是CMS就是以最短回收停顿时间为目标的收集器,这就与设计的初衷背道而驰。此时CMS的做法就是:先进行新生代的垃圾回收(有那个young GC),在进行老年代的回收,这样就可以提高效率。
CMS中与此相关联的四个参数
- CMSScheduleRemarkEdenSizeThreshold,默认值:2M
- CMSScheduleRemarkEdenPenetration,默认值:50%
- CMSMaxAbortablePrecleanTime,默认值:5s
- CMSScavengeBeforeRemark参数,使用remark前强制进行一次minor GC
当前Eden空间使用超过2M的时候启动可中止的并发预清理(CMS-concurrent-abortable-preclean),到Eden空间使用率达到50%的时候中止(但不是结束),进入Remark(重新标记阶段)。当发生minor GC就会结束并发预处理,但是我们无法控制minor GC发生的时机,这是由JVM自动触发的,所以不管有没有发生minor GC或者是Eden否达到50%,只要达到5s都会中断此阶段(因为如果到了5秒还是没有发生minor GC ,则会在Remark前强制进行一次Minor GC,通过CMSScavengeBeforeRemark控制),进入Remark。
记忆集
进行young GC时,GC Root除了栈引用、静态变量、常量、锁对象、class对象以外,如果老年代有对象引用了新生代对象,那么老年代的对象应该加入到GC Root 的范围中,但是如果每次进行young GC时都要扫描老年代的话代价太大,因此使用了一种叫做记忆集的抽象数据结构来记录这种引用关系。
记忆集是一种用于记录从非收集区域指向收集区域的指针集合的数据结构。
在不考虑效率和成本时,可以用一个数组存储所有指针指向新生代的老年代对象。但是如果这样的话维护成本就比较低。例如:所有的老年代对象都有指针指向了新生代,那么需要维护整个老年代大小的记忆集,毫无疑问这样是不可取的,因此就出现了卡表。
卡表
记忆集是我们针对跨代引用提出的思想,而卡表则是改思想的具体实现。
在hotspot虚拟机中,卡表是一个字节数组,数组中每一个元素对应着内存中的某一块连续地址区域,如果该区域中引用了待回收区域的对象,卡表数组对应的元素将被置为1,否则置为0。
- 卡表中使用了一个字节数组实现:CARD_TABLE[],每个元素对应着其标识的一块特定大小的内存块,称之为“卡页”。hotspot使用的卡页是2^9大小,即512字节。
- 一个卡页中包含多个对象,只要有一个对象的字段存在跨代指针,其对应的卡表的元素表示就是1,表示该元素变脏,否则为0。GC时,只要筛选本收集区的卡表中脏元素加入GC Root里。
卡表流程图
初始化标记
并发标记,A对象发生了所在的引用发生了变化,所有A对象所在的块被标记为脏卡。
重新标记,修改对象的引用,同时清除脏卡标记。
卡表其他作用
老年代识别新生代的时候,对应的Card table 被标识为相应的值,(Card table 中是一个byte,有8位,约定好每一位的含义就可以区分哪个是引用新生代,哪个是并发标记阶段修改过的)。
Foregroud CMS模式
这种模式是在并发失败才会执行的。
关于并发失败官方的描述
如果并发收集器不能在老年代填满之前完成不可大(unreachable)对象的回收,或者老年代有效的空间内存不满足某一个内存的分配请求,此时应用会被暂停,并在此暂停期间开始垃圾回收,直到回收完成才恢复应用程序。这种无法完成收集的情况就成为 并发模式失败(concurrent mode failure) ,而且这种情况的发生也意味着我们需要调节并发收集器的参数了。
简单来说,也就是我去进行并发标记的时候,内存不足。这个时候进入了STW,并且开始全局Full GC。
什么时候会进行并发失败呢?难道非要填满之后才能进行收集吗?
-XX:CMSInitiatingOccupancyFraction
-XX:+UserCMSInitiatingOccupancyOnly
注意:-XX:+UserCMSInitiatingOccupancyOnly只是用设定的回收阈值(上面指定的70%),如果不指定,JVM仅在第一次使用设定值,后续会则自动调整。这两个参数表示只有在old区占了CMSInitiatingOccupancyFraction设定的百分比的内存时才满足触发CMS的条件。注意这只是满足触发CMS GC 的条件。至于什么时候真正触发CMS GC,由一个后台扫描线程决定。CMSThread默认2秒扫描一次,判断是够需要触发CMS,这个参数可以更改这个扫描时间间隔。
源码公式
((100 - MinHeapFreeRatio) + (double)(CMSTriggerRatio * MinHeapFreeRatio) / 100.0) / 100.0
也是按照默认值
当老年代达到((100 - 40) + (double)80 * 40 / 100) / 100 = 92%时,才会出发CMS回收。
如何避免他的出现:
为了尽量避免并发模式失败发生,我们可以调节-XX:CMSInitiatingOccupancyFraction=<N>参数,去控制当老年代的内存占用达到多少的时候(N%),便开启并发收集器开始回收老年代。
CMS的标记压缩算法——MSC (Mark Sweep Compact)
他的回收方式其实就是我们滑动整理,并且进行整理的时候一般都是两个参数。
-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=0
这两个参数表示多少次Full GC后采用MSC算法压缩堆内存,0表示每次Full GC后都会压缩,同时0也是默认值。
碎片问题也是CMS采用的标记清理算法中最让人诟病的地方,Background CMS采用的标记清除算法会导致内存碎片问题,从而埋下发生Full GC导致长时间STW的隐患。所以如果出发Full GC,无论是否会采用MSC算法压缩,那都是ParNew+CMS组合非常糟糕的情况。因为这个时候并发模式已经搞不定了,而且整个过程单线程,完全STW,可能会压缩堆(是否压缩堆通过上面两个参数控制),真的不能在糟糕了!想象如果这个时候业务量比较大,由于Full GC导致服务完全暂停几秒钟,甚至上10秒,对用户体验影响极大。
三色标记
三色标记并非官方的概念,而是为了帮助更好理解并发标记。
在并发标记过程中,因为标记期间应用程序还在继续跑,对象间引用可能会发生变化,多标和漏标的情况就有可能会发生。这里引入三色标记来解释,GC Root 可达性分析遍历对象过程中遇到的对象,按照“是否访问过”这个条件标记成一下三种颜色:
黑色,表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描。黑色的对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无需重新扫描一
、遍,黑色对象不可能直接(不经过灰色对象)指向某个白色对象。
灰色,表示已经被垃圾收集器访问过,但这个对象上至少存在一个引用还没有被扫描。
白色,表示对象尚未被垃圾收集器访问过,显然在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束的阶段,仍然是白色的对象,即代表不可达。
标记过程
- 初始时,所有对象都在【白色集合】;
- 将GC Root 直接引用到的对象挪到【灰色集合】;
- 从【灰色集合】中获取对象;
- 将本对象引用到的其他对象全部挪到【灰色集合】;
- 将本对象挪到【黑色集合】;
重复步骤3、4,直至【灰色集合】为空时结束。
结束后,仍在【白色集合】的对象即为GC Root不可达,可以进行回收。
多标——浮动垃圾
在并发标记过程中,如果由于方法运行结束导致部分局部变量(GC Root)被销毁,这个GC Root 引用的对象之前又被扫描过(被标记为存活对象),那么本轮GC不会回收这部分内存。这部分本应该回收但是没有被回收到的内存,被称之为“浮动垃圾”。浮动垃圾并不影响回收的正确性,只是需要等待下一轮垃圾回收中才被清除。
另外,针对并发标记(还有被并发清理)开始后产生的新对象,通常的做法是直接全部当成黑色,本轮不会进行清除。这部分对象期间可能也会变成垃圾,这也算是浮动垃圾的一部分。
漏标——读写屏障
漏标只有同时满足一下两个条件是才会发生
- 条件一:灰色对象断开了白色对象的引用;即灰色对象原来的成员变量的引用发生了变化。
- 条件二:黑色对象重新引用该白色对象;即黑色对象成员变量增加了新的引用。
漏标会导致被引用的对象被当成垃圾误删除,这是严重的bug,必须解决。目前有两种解决方案:增量更新(Incremental Update)和原始快照(Snapshot At The Begining,SATB)。
增量更新
就是当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发扫描结束之后,再以这些记录过的引用关系中的黑色对象为根,重新扫描一次。可以理解为。黑色对象一旦新插入了指向白色对象的引用之后,它就会变回灰色对象。
原始快照
当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过引用关系的灰色对象为根,重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑色(目的就是让这种对象在本轮GC清理中存活下来,待下轮GC的时候重新扫描,这个对象也可能是浮动垃圾)。
无论是对引用关系记录的插入还是删除,虚拟机的记录操作都是通过写屏障实现的。
写屏障实现原始快照(STAB):当对象B的成员变量的引用发生变化时,比如引用消失(a.b.d=null),我们可以利用写屏障,将B原来的成员变量的引用对象D记录下来。
写屏障实现增量更新:当对象A的成员变量的引用发生变化时,比如新增引用(a.d=d),我们可以利用写屏障,将A新的成员变量引用对象D记录下来。
CMS标记清除的全局整理:
由于CMS使用的是标记清除算法,而标记清除算法会有大量的内存碎片的产生,所以JVM提供了
-XX:+UseCMSCompactAtFullCollection参数用于在全局GC(full GC)后进行一次碎片整理的工作,
由于每次全局GC后都进行碎片整理会较大的影响停顿时间,JVM又提供了参数
-XX:CMSFullGCsBeforeCompaction去 控制在几次全局GC后会进行碎片整理 。
CMS常用参数含义:
-XX:+UseConcMarkSweepGC
打开CMS GC收集器。JVM在1.8之前默认使用的是Parallel GC,9以后使用G1 GC。
-XX:+UseParNewGC
当使用CMS收集器时,默认年轻代使用多线程并行执行垃圾回收(UseConcMarkSweepGC开启后则默认开启)。
-XX:+CMSParallelRemarkEnabled
采用并行标记方式降低停顿(默认开启)。
-XX:+CMSConcurrentMTEnabled
被启用时,并发的CMS阶段将以多线程执行(因此,多个GC线程会与所有的应用程序线程并行工作)。(默认开启)
-XX:ConcGCThreads
定义并发CMS过程运行时的线程数。
-XX:ParallelGCThreads
定义CMS过程并行收集的线程数。
-XX:CMSInitiatingOccupancyFraction
该值代表老年代堆空间的使用率,默认值为68。当老年代使用率达到此值之后,并行收集器便开始进行垃圾收集,该参数需要配合UseCMSInitiatingOccupancyOnly一起使用,单独设置无效。
-XX:+UseCMSInitiatingOccupancyOnly
该参数启用后,参数CMSInitiatingOccupancyFraction才会生效。默认关闭。
-XX:+CMSClassUnloadingEnabled
相对于并行收集器,CMS收集器默认不会对永久代进行垃圾回收。如果希望对永久代进行垃圾回收,可用设置-XX:+CMSClassUnloadingEnabled。默认关闭。
-XX:+CMSIncrementalMode
开启CMS收集器的增量模式。增量模式使得回收过程更长,但是暂停时间往往更短。默认关闭。
-XX:CMSFullGCsBeforeCompaction
设置在执行多少次Full GC后对内存空间进行压缩整理,默认值0。
-XX:+CMSScavengeBeforeRemark
在cms gc remark之前做一次ygc,减少gc roots扫描的对象数,从而提高remark的效率,默认关闭。
-XX:+ExplicitGCInvokesConcurrent
该参数启用后JVM无论什么时候调用系统GC,都执行CMS GC,而不是Full GC。
-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses
该参数保证当有系统GC调用时,永久代也被包括进CMS垃圾回收的范围内。
-XX:+DisableExplicitGC
该参数将使JVM完全忽略系统的GC调用(不管使用的收集器是什么类型)。
-XX:+UseCompressedOops
这个参数用于对类对象数据进行压缩处理,提高内存利用率。(默认开启)
-XX:MaxGCPauseMillis=200
这个参数用于设置GC暂停等待时间,单位为毫秒,不要设置过低。
CMS的线程数计算公式
区分young区的parnew gc线程数和old区的cms线程数,分别为以下两参数:
-
-XX:ParallelGCThreads=m // STW暂停时使用的GC线程数,一般用满CPU
-
-XX:ConcGCThreads=n // GC线程和业务线程并发执行时使用的GC线程数,一般较小
ParallelGCThreads
其中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
ConcGCThreads
ConcGCThreads的默认值则为:
ConcGCThreads = (ParallelGCThreads + 3)/4 向下取整。
-
ParallelGCThreads = 1~4时,ConcGCThreads = 1
-
ParallelGCThreads = 5~8时,ConcGCThreads = 2
-
ParallelGCThreads = 13~16时,ConcGCThreads = 4
CMS推荐配置参数:
第一种情况:8C16G左右服务器,再大的服务器可以上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
第二种情况:4C8G
-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
第三种情况:2C4G,这种情况下,也不推荐使用,因为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