CMS垃圾回收器

一、简介

Concurrent Mark Sweep简称CMS,是一种以获取最短回收停顿时间为目标的收集器,尤其重视服务的响应速度。

CMS是老年代垃圾回收器,基于标记-清除算法实现,只回收老年代和永久代(jdk 1.8开始为元数据区,需要设置CMSClassUnloadingEnabled),一般配合perNew使用。

CMS是一种预处理垃圾回收器,它不能等到old内存用尽时回收,需要在内存用尽前,完成回收操作,否则会导致并发回收失败;所以CMS垃圾回收器开始执行回收操作,有一个触发阈值,默认是老年代或永久代达到92%

二、工作原理

CMS是最常用的垃圾垃圾回收器之一,下面分析下CMS垃圾回收器工作原理。

2.1 处理过程

  1. 初始标记(CMS-initial-mark),会导致STW
  2. 并发标记(CMS-concurrent-mark),与用户线程同时运行;
  3. 预清理(CMS-concurrent-preclean),与用户线程同时运行;
  4. 可被终止的预清理(CMS-concurrent-abortable-preclean),与用户线程同时运行;
  5. 重新标记(CMS-remark),会导致STW
  6. 并发清理(CMS-concurrent-sweep),与用户线程同时运行;
  7. 并发重置状态等待下次CMS的触发(CMS-concurrent-reset),与用户线程同时运行;

其中初始标记、并发标记、重新标记和并发清理这四个比较重要。

CMS运行流程图如下所示:

20200329200433211.png

下面抓取一下GC信息,来进行详细分析,首先将JVM中加入以下运行参数:

-XX:+PrintCommandLineFlags                  [1]
-XX:+UseConcMarkSweepGC                     [2]
-XX:+UseCMSInitiatingOccupancyOnly          [3]
-XX:CMSInitiatingOccupancyFraction=80       [4]
-XX:+CMSClassUnloadingEnabled               [5]
-XX:+UseParNewGC                            [6]
-XX:+CMSParallelRemarkEnabled               [7]
-XX:+CMSScavengeBeforeRemark                [8]
-XX:+UseCMSCompactAtFullCollection          [9]
-XX:CMSFullGCsBeforeCompaction=0            [10]
-XX:+CMSConcurrentMTEnabled                 [11]
-XX:ConcGCThreads=4                         [12]
-XX:+ExplicitGCInvokesConcurrent            [13]
-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses    [14]
-XX:+CMSParallelInitialMarkEnabled          [15]
-XX:+PrintGCDetails                         [16]
-XX:+PrintGCCause                           [17]
-XX:+PrintGCTimeStamps                      [18]
-XX:+PrintGCDateStamps                      [19]
-Xloggc:../logs/gc.log                      [20]
-XX:+HeapDumpOnOutOfMemoryError             [21]
-XX:HeapDumpPath=../dump                    [22]

先来介绍下下面几个参数的作用:

  1. [1]打印出启动参数行;

  2. [2]参数指定使用CMS垃圾回收器;

  3. [3]、[4]参数指定CMS垃圾回收器在老年代达到80%的时候开始工作,如果不指定那么默认的值为92%

  4. [5]开启永久代(jdk 1.8以下版本)或元数据区(jdk 1.8及其以上版本)收集,如果没有设置这个标志,一旦永久代或元数据区耗尽空间也会尝试进行垃圾回收,但是收集不会是并行的,而再一次进行Full GC

  5. [6]使用CMS时默认这个参数就是打开的,不需要配置,CMS只回收老年代,年轻代只能配合Parallel NewSerial回收器;

  6. [7]减少Remark阶段暂停的时间,启用并行Remark,如果Remark阶段暂停时间长,可以启用这个参数

  7. [8]如果Remark阶段暂停时间太长,可以启用这个参数,在Remark执行之前,先做一次ygc。因为这个阶段,年轻代也是CMSgcrootCMS会扫描年轻代指向老年代对象的引用,如果年轻代有大量引用需要被扫描,会让Remark阶段耗时增加;

  8. [9]、[10]两个参数是针对CMS垃圾回收器碎片做优化的,CMS是不会移动内存的,运行时间长了,会产生很多内存碎片,导致没有一段连续区域可以存放大对象,出现”promotion failed”、”concurrent mode failure”,导致fullgc,启用UseCMSCompactAtFullCollectionFull GC的时候,对年老代的内存进行压缩。-XX:CMSFullGCsBeforeCompaction=0则是代表多少次FGC后对老年代做压缩操作,默认值为0,代表每次都压缩,把对象移动到内存的最左边,可能会影响性能,但是可以消除碎片;

    106.641: [GC 106.641: [ParNew (promotion failed): 14784K->14784K(14784K), 0.0370328 secs]106.678: [CMS106.715: [CMS-concurrent-mark: 0.065/0.103 secs] [Times: user=0.17 sys=0.00, real=0.11 secs]
    (concurrent mode failure): 41568K->27787K(49152K), 0.2128504 secs] 52402K->27787K(63936K), [CMS Perm : 2086K->2086K(12288K)], 0.2499776 secs] [Times: user=0.28 sys=0.00, real=0.25 secs]

  9. [11]定义并发CMS过程运行时的线程数。比如value=4意味着CMS周期的所有阶段都以4个线程来执行。尽管更多的线程会加快并发CMS过程,但其也会带来额外的同步开销。因此,对于特定的应用程序,应该通过测试来判断增加CMS线程数是否真的能够带来性能的提升。如果未设置这个参数,JVM会根据并行收集器中的-XX:ParallelGCThreads参数的值来计算出默认的并行CMS线程数( ncpuscpu个数):\(ParallelGCThreads = (ncpus <= 8 ? ncpus : 8 + (ncpus - 8)* 5/8)\)\(ConcGCThreads = (ParallelGCThreads + 3)/4\),这个参数一般不要自己设置,使用默认就好,除非发现默认的参数有调整的必要;

  10. [13]、[14]开启foreground CMS GCCMS gc有两种模式,backgroundforeground,正常的CMS GC使用background模式,就是我们平时说的CMS GC;当并发收集失败或者调用了System.gc()的时候,就会导致一次Full GC,这个Full GC是不是CMS回收,而是Serial单线程回收器,加入了参数[12]后,执行Full GC的时候,就变成了CMS foreground gc,它是并行Full GC,只会执行CMSstop the world阶段的操作,效率比单线程Serial full GC要高;需要注意的是它只会回收old,因为CMS收集器是老年代收集器;而正常的Serial收集是包含整个堆的,加入了参数[14],代表永久代也会被CMS收集;

  11. [15]开启初始标记过程中的并行化,进一步提升初始化标记效率;

  12. [16]、[17]、[18]、[19]、[20]是打印gc日志,其中[16]在jdk1.8之后无需设置;

  13. [21]、[22]则是内存溢出时dump堆。

下面就是该参数设置打印出来的gc信息,一些非关键的信息已经去掉,如时间:

//第一步 初始标记 这一步会停顿
[GC (CMS Initial Mark) [1 CMS-initial-mark: 299570K(307200K)] 323315K(491520K), 0.0026208 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
vmop                    [threads: total initially_running wait_to_block]    [time: spin block sync cleanup vmop] page_trap_count
0.345: CMS_Initial_Mark                 [      10          0              1    ]      [     0     0     0     0     2    ]  0
Total time for which application threads were stopped: 0.0028494 seconds

//第二步 并发标记
[CMS-concurrent-mark-start]
[CMS-concurrent-mark: 0.012/0.012 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]

//第三步 预清理
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

//第四步 可被终止的预清理
[CMS-concurrent-abortable-preclean-start]
[CMS-concurrent-abortable-preclean: 0.000/0.000 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

//第五步 重新标记
[GC (CMS Final Remark) [YG occupancy: 72704 K (184320 K)][Rescan (parallel) , 0.0009069 secs][weak refs processing, 0.0000083 secs][class unloading, 0.0002626 secs][scrub symbol table, 0.0003789 secs][scrub string table, 0.0001326 secs][1 CMS-remark: 299570K(307200K)] 372275K(491520K), 0.0017842 secs] [Times: user=0.05 sys=0.00, real=0.00 secs]
vmop                    [threads: total initially_running wait_to_block]    [time: spin block sync cleanup vmop] page_trap_count
0.360: CMS_Final_Remark                 [      10          0              1    ]      [     0     0     0     0     1    ]  0
Total time for which application threads were stopped: 0.0018800 seconds

//第六步 清理
[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.007/0.007 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]

//第七步 重置
[CMS-concurrent-reset-start]
[CMS-concurrent-reset: 0.002/0.002 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

2.2 初始标记

这是CMS中两次stop-the-world事件中的一次。这一步的作用是标记存活的对象,有两部分:

  1. 标记老年代中所有的GC Roots对象,如下图节点1;
  2. 标记年轻代中活着的对象引用到的老年代的对象(指的是年轻代中还存活的引用类型对象,引用指向老年代中的对象)如下图节点2、3;

20170502172953141.png

ps:为了加快此阶段处理速度,减少停顿时间,可以开启初始标记并行化,-XX:+CMSParallelInitialMarkEnabled,同时调大并行标记的线程数,线程数不要超过cpu的核数。

2.3 并发标记

从“初始标记”阶段标记的对象开始找出所有存活的对象;

因为是并发运行的,在运行期间会发生新生代的对象晋升到老年代、或者是直接在老年代分配对象、或者更新老年代对象的引用关系等等,对于这些对象,都是需要进行重新标记的,否则有些对象就会被遗漏,发生漏标的情况。为了提高重新标记的效率,该阶段会把上述对象所在的Card标识为Dirty,后续只需扫描这些Dirty Card的对象,避免扫描整个老年代;

并发标记阶段只负责将引用发生改变的Card标记为Dirty状态,不负责处理;

如下图所示,也就是节点1、2、3,最终找到了节点4和5。并发标记的特点是和应用程序线程同时运行。并不是老年代的所有存活对象都会被标记,因为标记的同时应用程序会改变一些对象的引用等。

20170502175211859.png

这个阶段因为是并发的容易导致concurrent mode failure

2.4 预清理阶段

前一个阶段已经说明,不能标记出老年代全部的存活对象,是因为标记的同时应用程序会改变一些对象引用,这个阶段就是用来处理前一个阶段因为引用关系改变导致没有标记到的存活对象的,它会扫描所有标记为DiretyCard

如下图所示,在并发清理阶段,节点3的引用指向了6;则会把节点3的card标记为Dirty

20170502211600103.png

最后将6标记为存活,如下图所示:

20170502211950472.png

2.5 可终止的预处理

这个阶段尝试着去承担下一个阶段Final Remark阶段足够多的工作。这个阶段持续的时间依赖好多的因素,由于这个阶段是重复的做相同的事情直到发生aboart的条件(比如:重复的次数、多少量的工作、持续的时间等等)之一才会停止。

ps:此阶段最大持续时间为5秒,之所以可以持续5秒,另外一个原因也是为了期待这5秒内能够发生一次ygc,清理年轻代的引用,是的下个阶段的重新标记阶段,扫描年轻代指向老年代的引用的时间减少;

2.6 重新标记

这个阶段会导致第二次stop the word,该阶段的任务是完成标记整个年老代的所有的存活对象。

这个阶段,重新标记的内存范围是整个堆,包含young_genold_gen。为什么要扫描新生代呢,因为对于老年代中的对象,如果被新生代中的对象引用,那么就会被视为存活对象,即使新生代的对象已经不可达了,也会使用这些不可达的对象当做CMSgc root,来扫描老年代;因此对于老年代来说,引用了老年代中对象的新生代的对象,也会被老年代视作GC ROOTS:当此阶段耗时较长的时候,可以加入参数-XX:+CMSScavengeBeforeRemark,在重新标记之前,先执行一次ygc,回收掉年轻代的对象无用的对象,并将对象放入幸存代或晋升到老年代,这样再进行年轻代扫描时,只需要扫描幸存区的对象即可,一般幸存代非常小,这大大减少了扫描时间。

由于之前的预处理阶段是与用户线程并发执行的,这时候可能年轻代的对象对老年代的引用已经发生了很多改变,这个时候,remark阶段要花很多时间处理这些改变,会导致很长stop the word,所以通常CMS尽量运行Final Remark阶段在年轻代是足够干净的时候。

另外,还可以开启并行收集:-XX:+CMSParallelRemarkEnabled

2.7 并发清理

通过以上5个阶段的标记,老年代所有存活的对象已经被标记并且现在要通过Garbage Collector采用清扫的方式回收那些不能用的对象了。

这个阶段主要是清除那些没有标记的对象并且回收空间;

2020030722171696.png

由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。

2.8 并发重置

这个阶段并发执行,重新设置CMS算法内部的数据结构,准备下一个CMS生命周期的使用。

CMS并发GC不是Full GC
HotSpot VM里对concurrent collectionfull collection有明确的区分。所有带有full collection字样的VM参数都是跟真正的Full GC相关,而跟CMS并发GC无关的,CMS收集算法只是清理老年代。

三、三色标记算法

3.1 基本算法

要找出存活对象,根据可达性分析,从GC Roots开始进行遍历访问,可达的则为存活对象。将遍历对象图过程中遇到的对象,按“是否访问过”这个条件标记成以下三种颜色:

  • 白色:未被标记的对象
  • 黑色:本对象和成员变量都被标记
  • 灰色:本对象已被标记,成员对象未被标记。当全部标记后,会转换为黑色

假设现在有白、灰、黑三个集合(表示当前对象的颜色),其遍历访问过程为:

  1. 初始时,所有对象都在【白色集合】中;
  2. GC Roots直接引用到的对象挪到【灰色集合】中;
  3. 从灰色集合中获取对象:
    3.1. 将本对象引用到的其他对象全部挪到【灰色集合】中;
    3.2. 将本对象挪到【黑色集合】里面。
  4. 重复步骤3,直至【灰色集合】为空时结束。
  5. 结束后,仍在【白色集合】的对象即为GC Roots不可达,可以进行回收。

注:如果标记结束后对象仍为白色,意味着已经“找不到”该对象在哪了,不可能会再被重新引用。

Stop The WorldSTW)时,对象间的引用是不会发生变化的,可以轻松完成标记。而当需要支持并发标记时,即标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生。

3.2 多标-浮动垃圾

在并发标记过程中,如果由于方法运行结束导致部分局部变量(GC Root)被销毁,这个GC Root引用的对象之前又被扫描过(被标记为非垃圾对象),那么本轮GC不会回收这部分内存。这部分本应该回收但是没有回收到的内存,被称之为"浮动垃圾"。浮动垃圾并不会影响垃圾回收的正确性,只是需要等到下一轮垃圾回收中才被清除。

另外,针对并发标记(还有并发清理)开始后产生的新对象,通常的做法是直接全部当成黑色,本轮不会进行清除。这部分对象期间可能也会变为垃圾,这也算是浮动垃圾的一部分。

3.3 漏标-读写屏障

多标无伤大雅,但是漏标就会造成重大错误了,会导致应该存货的对象被当成垃圾回收掉,这是绝对不允许的,下面举例说明漏标的情况:

(1) 最开始A指向BB指向D,但是在A完成标记后,标记到了B还没标记D时,B指向D的引用消失,而A又指向了D,此时因为A标记为黑色表示所有子对象都被标记,所以D不会被检测到,从而导致漏标。如下图:

为了解决上诉问题,CMS给出了自己的解决方案,就是一个对象A的引用指向了另一个对象D,那么将这个对象A重新标记为灰色(这种方式也有bug,见下文),如下:

(2) 由CMSIncremental Update而产生的漏标问题。如下图:

要出现上诉情况分为以下几步:

(1) GC线程m1正在标记对象A,并且对象A中的属性1已经完成标记,还未完成属性2的标记,所以此时对象A为灰色。
(2) 此时业务逻辑线程m2将对象A的属性1指向对象D,根据Incremental Update规则,此时A应该被GC线程m2更改为灰色,但是因为对象A的属性2并未完成标记,所以A本来就是灰色,并没有变化。
(3) GC线程m1完成了对对象A的属性2的标记,因为对象A的属性1在线程m1看来已经完成了标记,而不会再去看属性1是否有引用到对象D,所以将对象A标记为黑色。所以对象D被漏标了。

四、CMS存在的问题

4.1 减少remark阶段停顿

一般CMSGC耗时80%都在remark阶段,如果发现remark阶段停顿时间很长,可以尝试添加该参数:

-XX:+CMSScavengeBeforeRemark

在执行remark操作之前先做一次Young GC,目的在于减少年轻代对老年代的无效引用,降低remark时的开销,如果添加该参数后\(ygc停顿时间 + remark时间 < 添加该参数之前的remark时间\),说明该参数是有效的。

4.2 内存碎片

CMS是基于标记-清除算法的,只会将标记为为存活的对象删除,并不会移动对象整理内存空间,会造成内存碎片,这时候我们需要用到这个参数;

-XX:+UseCMSCompactAtFullCollection (空间碎片整理)
-XX:CMSFullGCsBeforeCompaction=n

这个参数大部分人的使用方式都是错误的,往往会导致设置后问题更大。
CMSFullGCsBeforeCompaction这个参数在HotSpot VM里是这样声明的:

product(bool, UseCMSCompactAtFullCollection, true,
"Use mark sweep compact at full collections")
product(uintx, CMSFullGCsBeforeCompaction, 0,
"Number of CMS full collection done before compaction if > 0")

然后这样使用的:

*should_compact =
UseCMSCompactAtFullCollection &&
((_full_gcs_since_conc_gc >= CMSFullGCsBeforeCompaction) ||
GCCause::is_user_requested_gc(gch->gc_cause()) ||
gch->incremental_collection_will_fail(true /* consult_young */));

CMS GC要决定是否在Full GC时做压缩,会依赖几个条件。其中:

  1. UseCMSCompactAtFullCollectionCMSFullGCsBeforeCompaction是搭配使用的;前者目前默认就是true了,也就是关键在后者上。
  2. 用户调用了System.gc(),而且DisableExplicitGC没有开启。
  3. young gen报告接下来如果做增量收集会失败;简单来说也就是young gen预计old gen没有足够空间来容纳下次young GC晋升的对象。

上述三种条件的任意一种成立都会让CMS决定这次做Full GC时要做压缩。

CMSFullGCsBeforeCompaction说的是,在上一次CMS并发GC执行过后,到底还要再执行多少次Full GC才会做压缩。默认是0,也就是在默认配置下每次CMS GC顶不住了而要转入Full GC的时候都会做压缩。 如果把CMSFullGCsBeforeCompaction配置为10,就会让上面说的第一个条件变成每隔10次真正的Full GC才做一次压缩(而不是每10CMS并发GC就做一次压缩,目前VM里没有这样的参数)。这会使Full GC更少做压缩,也就更容易使CMSold gen受碎片化问题的困扰。本来这个参数就是用来配置降低Full GC压缩的频率,以期减少某些Full GC的暂停时间。CMS回退到Full GC时用的算法是mark-sweep-compact,但compaction是可选的,不做的话碎片化会严重些但这次Full GC的暂停时间会短些,这是个取舍。

4.3 concurrent mode failure(并发模式失败)

这个异常发生在CMS正在回收的时候。执行CMS GC的过程中,同时业务线程也在运行,当年轻代空间满了,执行ygc时,需要将存活的对象放入到老年代,而此时老年代空间不足,这时CMS还没有机会回收老年代产生的,或者在做Minor GC的时候,新生代救助空间放不下,需要放入老年代,而老年代也放不下而产生的。

设置CMS触发时机有两个参数:

-XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=70

-XX:CMSInitiatingOccupancyFraction=70是指设定CMS在对内存占用率达到70%的时候开始GC
-XX:+UseCMSInitiatingOccupancyOnly如果不指定,只是用设定的回收阈值CMSInitiatingOccupancyFraction,则JVM仅在第一次使用设定值,后续则自动调整会导致上面的那个参数不起作用。

为什么要有这两个参数?

由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。

CMS前五个阶段都是标记存活对象的,除了”初始标记”和”重新标记”阶段会stop the word,其它三个阶段都是与用户线程一起跑的,就会出现这样的情况gc线程正在标记存活对象,用户线程同时向老年代提升新的对象,清理工作还没有开始,old gen已经没有空间容纳更多对象了,这时候就会导致concurrent mode failure, 然后就会使用串行收集器回收老年代的垃圾,导致停顿的时间非常长。

CMSInitiatingOccupancyFraction参数要设置一个合理的值,设置大了,会增加concurrent mode failure发生的频率,设置的小了,又会增加CMS频率,所以要根据应用的运行情况来选取一个合理的值。

如果发现这两个参数设置大了会导致Full GC,设置小了会导致频繁的CMS GC,说明你的老年代空间过小,应该增加老年代空间的大小了;

4.4 promotion failed(晋升失败)

这个异常发生在年轻代回收的时候。在进行Minor GC时,Survivor Space放不下,对象只能放入老年代,而此时老年代也放不下造成的,多数是由于老年代有足够的空闲空间,但是由于碎片较多,新生代要转移到老年代的对象比较大,找不到一段连续区域存放这个对象导致的,以下是一段promotion failed的日志:

106.641: [GC 106.641: [ParNew (promotion failed): 14784K->14784K(14784K), 0.0370328 secs]106.678: [CMS106.715: [CMS-concurrent-mark: 0.065/0.103 secs] [Times: user=0.17 sys=0.00, real=0.11 secs]
(concurrent mode failure): 41568K->27787K(49152K), 0.2128504 secs] 52402K->27787K(63936K), [CMS Perm : 2086K->2086K(12288K)], 0.2499776 secs] [Times: user=0.28 sys=0.00, real=0.25 secs]

4.4.1 过早提升与提升失败

Minor GC过程中,Survivor Unused可能不足以容纳Eden和另一个Survivor中的存活对象,那么多余的将被移到老年代,称为过早提升(Premature Promotion),这会导致老年代中短期存活对象的增长,可能会引发严重的性能问题。再进一步,如果老年代满了,Minor GC后会进行Full GC,这将导致遍历整个堆,称为提升失败(Promotion Failure)。

早提升的原因

  1. Survivor空间太小,容纳不下全部的运行时短生命周期的对象,如果是这个原因,可以尝试将Survivor调大,否则端生命周期的对象提升过快,导致老年代很快就被占满,从而引起频繁的Full GC
  2. 对象太大,SurvivorEden没有足够大的空间来存放这些大象;

提升失败原因

当提升的时候,发现老年代也没有足够的连续空间来容纳该对象。
为什么是没有足够的连续空间而不是空闲空间呢?

老年代容纳不下提升的对象有两种情况:

  1. 老年代空闲空间不够用了;
  2. 老年代虽然空闲空间很多,但是碎片太多,没有连续的空闲空间存放该对象;

解决方法

  1. 如果是因为内存碎片导致的大对象提升失败,CMS需要进行空间整理压缩;
  2. 如果是因为提升过快导致的,说明Survivor空闲空间不足,那么可以尝试调大Survivor
  3. 如果是因为老年代空间不够导致的,尝试将CMS触发的阈值调低;

4.5 其它导致回收停顿时间变长原因

linux使用了swap,内存换入换出(vmstat),尤其是开启了大内存页的时候,因为swap只支持4k的内存页,大内存页的大小为2M,大内存页在swap的交换的时候需要先将swap4k内存页合并成一个大内存页再放入内存或将大内存页切分为4k的内存页放入swap,合并和切分的操作会导致操作系统占用cup飙高,用户cpu占用反而很低;
除了swap交换外,网络ionetstat)、磁盘I/Oiostat)在GC过程中发生会使GC时间变长。
如果是以上原因,就要去查看gc日志中的Times耗时:

[Times: user=0.00 sys=0.00, real=0.00 secs]

user是用户线程占用的时间,sys是系统线程占用的时间,如果是io导致的问题,会有两种情况:

1.usersys时间都非常小,但是real却很长,如下:

[ Times: user=0.51 sys=0.10, real=5.00 secs ]

user+sys的时间远远小于real的值,这种情况说明停顿的时间并不是消耗在cup执行上了,不是cup肯定就是io导致的了,所以这时候要去检查系统的io情况。

2.sys时间很长,user时间很短,real几乎等于sys的时间,如下:

[ Times: user=0.11 sys=31.10, real=33.12 secs ]

这时候其中一种原因是开启了大内存页,还开启了swap,大内存进行swap交换时会有这种现象。

4.6 增加线程数

CMS默认启动的回收线程数目是\((ParallelGCThreads + 3) / 4\),这里的ParallelGCThreads是年轻代的并行收集线程数;

年轻代的并行收集线程数默认是(ncpus <= 8) ? ncpus : 3 + ((ncpus * 5) / 8),可以通过-XX:ParallelGCThreads=N来调整;

如果要直接设定CMS回收线程数,可以通过-XX:ParallelCMSThreads=n,注意这个n不能超过cpu线程数,需要注意的是增加gc线程数,就会和应用争抢资源。

五、CMS收集器调优

下面大多数与CMS收集器调优相关的JVM标志参数。

-XX:+UseConcMarkSweepGC

该标志首先是激活CMS收集器。默认HotSpot JVM使用的是并行收集器。

-XX:+UseParNewGC

当使用CMS收集器时,该标志激活年轻代使用多线程并行执行垃圾回收。这令人很惊讶,我们不能简单在并行收集器中重用-XX:+UserParNewGC标志,因为概念上年轻代用的算法是一样的。然而,对于CMS收集器,年轻代GC算法和老年代GC算法是不同的,因此年轻代GC有两种不同的实现,并且是两个不同的标志。

注意最新的JVM版本,当使用-XX:+UseConcMarkSweepGC时,-XX:+UseParNewGC会自动开启。因此,如果年轻代的并行GC不想开启,可以通过设置-XX:-UseParNewGC来关掉。

-XX:+CMSConcurrentMTEnabled

当该标志被启用时,并发的CMS阶段将以多线程执行(因此,多个GC线程会与所有的应用程序线程并行工作)。该标志已经默认开启,如果顺序执行更好,这取决于所使用的硬件,多线程执行可以通过-XX:-CMSConcurremntMTEnabled禁用。

-XX:ConcGCThreads

标志-XX:ConcGCThreads=<value>(早期JVM版本也叫-XX:ParallelCMSThreads)定义并发CMS过程运行时的线程数。比如value=4意味着CMS周期的所有阶段都以4个线程来执行。尽管更多的线程会加快并发CMS过程,但其也会带来额外的同步开销。因此,对于特定的应用程序,应该通过测试来判断增加CMS线程数是否真的能够带来性能的提升。

如果还标志未设置,JVM会根据并行收集器中的-XX:ParallelGCThreads参数的值来计算出默认的并行CMS线程数。该公式是\(ConcGCThreads = (ParallelGCThreads + 3) / 4\)。因此,对于CMS收集器,-XX:ParallelGCThreads标志不仅影响stop-the-world垃圾收集阶段,还影响并发阶段。

总之,有不少方法可以配置CMS收集器的多线程执行。正是由于这个原因,建议第一次运行CMS收集器时使用其默认设置,然后如果需要调优再进行测试。只有在生产系统中测量(或类生产测试系统)发现应用程序的暂停时间的目标没有达到,就可以通过这些标志应该进行GC调优。

-XX:CMSInitiatingOccupancyFraction

当堆满之后,并行收集器便开始进行垃圾收集。例如,当没有足够的空间来容纳新分配或提升的对象。对于CMS收集器,长时间等待是不可取的,因为在并发垃圾收集期间应用持续在运行(并且分配对象)。因此,为了在应用程序使用完内存之前完成垃圾收集周期,CMS收集器要比并行收集器更先启动。

因为不同的应用会有不同对象分配模式,JVM会收集实际的对象分配(和释放)的运行时数据,并且分析这些数据,来决定什么时候启动一次CMS垃圾收集周期。为了引导这一过程,JVM会在一开始执行CMS周期前作一些线索查找。该线索由-XX:CMSInitiatingOccupancyFraction=<value>来设置,该值代表老年代堆空间的使用率。比如,value=75意味着第一次CMS垃圾收集会在老年代被占用75%时被触发。通常CMSInitiatingOccupancyFraction的默认值为68(之前很长时间的经历来决定的)。

-XX:+UseCMSInitiatingOccupancyOnly

我们用-XX:+UseCMSInitiatingOccupancyOnly标志来命令JVM不基于运行时收集的数据来启动CMS垃圾收集周期。而是,当该标志被开启时,JVM通过CMSInitiatingOccupancyFraction的值进行每一次CMS收集,而不仅仅是第一次。然而,请记住大多数情况下,JVM比我们自己能作出更好的垃圾收集决策。因此,只有当我们充足的理由(比如测试)并且对应用程序产生的对象的生命周期有深刻的认知时,才应该使用该标志。

-XX:+CMSClassUnloadingEnabled

相对于并行收集器,CMS收集器默认不会对永久代进行垃圾回收。如果希望对永久代进行垃圾回收,可用设置标志-XX:+CMSClassUnloadingEnabled。在早期JVM版本中,要求设置额外的标志-XX:+CMSPermGenSweepingEnabled。注意,即使没有设置这个标志,一旦永久代耗尽空间也会尝试进行垃圾回收,但是收集不会是并行的,而再一次进行Full GC

-XX:+CMSIncrementalMode

该标志将开启CMS收集器的增量模式。增量模式经常暂停CMS过程,以便对应用程序线程作出完全的让步。因此,收集器将花更长的时间完成整个收集周期。因此,只有通过测试后发现正常CMS周期对应用程序线程干扰太大时,才应该使用增量模式。由于现代服务器有足够的处理器来适应并发的垃圾收集,所以这种情况发生得很少。

-XX:+ExplicitGCInvokesConcurrent and -XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses

如今,被广泛接受的最佳实践是避免显式地调用GC(所谓的“系统GC”),即在应用程序中调用system.gc()。然而,这个建议是不管使用的GC算法的,值得一提的是,当使用CMS收集器时,系统GC将是一件很不幸的事,因为它默认会触发一次Full GC。幸运的是,有一种方式可以改变默认设置。标志-XX:+ExplicitGCInvokesConcurrent命令JVM无论什么时候调用系统GC,都执行CMS GC,而不是Full GC。第二个标志-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses保证当有系统GC调用时,永久代也被包括进CMS垃圾回收的范围内。因此,通过使用这些标志,我们可以防止出现意料之外的"stop-the-world"的系统GC

-XX:+DisableExplicitGC

然而在这个问题上…这是一个很好提到-XX:+ DisableExplicitGC标志的机会,该标志将告诉JVM完全忽略系统的GC调用(不管使用的收集器是什么类型)。对于我而言,该标志属于默认的标志集合中,可以安全地定义在每个JVM上运行,而不需要进一步思考。

参考文章

posted @ 2022-04-22 18:04  夏尔_717  阅读(1037)  评论(0编辑  收藏  举报