Java垃圾回收详解(4)

CMS 垃圾回收器详解之一 CMS 处理流程

Posted by Jason Lee on 2020-08-10

CMS 垃圾回收器简介

由于上一篇博文已经详细介绍了,这里就不在赘述,有兴趣可以围观一下Java垃圾回收详解(2)
GC方式介绍

这里做一个简单的回顾:

  • 获取最短回收停顿时间为目标的多线程并发收集器

  • cms 只会回收老年代和永久带(1.8开始为元数据区,需要设置- CMSClassUnloadingEnabled, 不会收集年轻带

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

CMS 过程详解

CMS收集器的7个阶段:

  • 初始标记(CMS initial mark)

  • 并发标记(CMS concurrent mark)

  • 并发预清理(CMS concurrent-preclean)

  • 并发可取消的预清理 (CMS concurrent-abortable-preclean)

  • 重新标记(CMS remark)

  • 并发清除(CMS concurrent sweep)

  • 并发重置(CMS concurrent-reset)

  • 流程大致如下

  • 具体过程如下

CMS 阶段详解

初始标记(Idling)阶段

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

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

在Java语言里,可作为GC Roots对象的包括如下几种:

  1. 虚拟机栈(栈桢中的本地变量表)中的引用的对象 ;
  2. 方法区中的类静态属性引用的对象 ;
  3. 方法区中的常量引用的对象 ;
  4. 本地方法栈中JNI的引用的对象;

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

并发标记 (InitialMarking)阶段

从“初始标记”阶段标记的对象开始找出所有存活的对象,也就是从GC_ROOTS 触发,标记所有存活对象,由于第一次的结果,所以这次的标记并不需要STW

因为是并发运行的,在运行期间会发生新生代的对象晋升到老年代、或者是直接在老年代分配对象、或者更新老年代对象的引用关系等等,对于这些对象,都是需要进行重新标记的

否则有些对象就会被遗漏,发生漏标的情况。为了提高重新标记的效率,该阶段会把上述对象所在的Card标识为Dirty,后续只需扫描这些Dirty Card的对象,避免扫描整个老年代;
并发标记阶段只负责将引用发生改变的Card标记为Dirty状态,不负责处理;

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

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

  • Card Table 是什么

HotSpot 的一项叫做卡表(Card Table)的技术。该技术将整个堆划分为一个个大小为 512 字节的卡,并且维护一个卡表,用来存储每张卡的一个标识位。这个标识位代表对应的卡是否可能存有指向新生代对象的引用。如果可能存在,那么我们就认为这张卡是脏的。在进行 Minor GC 的时候,我们便可以不用扫描整个老年代,而是在卡表中寻找脏卡,并将脏卡中的对象加入到 Minor GC 的 GC Roots 里。当完成所有脏卡的扫描之后,Java 虚拟机便会将所有脏卡的标识位清零。

预清理 (Precleaning) 阶段

通过参数 CMSPrecleaningEnabled 选择关闭该阶段,默认启用,主要做两件事情:

  1. 处理新生代已经发现的引用,比如在并发阶段,在Eden区中分配了一个A对象,A对象引用了一个老年代对象B(这个B之前没有被标记),在这个阶段就会标记对象B为活跃对象。

  2. 在并发标记阶段,如果老年代中有对象内部引用发生变化,会把所在的Card标记为Dirty,然后扫描这些dirty card

可中断的预清理(AbortablePreclean)阶段

该阶段发生是有前提的:

  • CMS 由于是在老年代垃圾扫描,但是大部分的老年代对象都是被 GC_ROOTS 引用的,如上图的 current obj 这个对象,虽然在老年代,但是其引用还是在年轻带,并且在并发标记阶段,也有可能老年代的某些对象别重新被新生代的对象重新引用。所以在gc的时候不仅需要扫描老年代也需要扫描新生代确定 GC_ROOTS

  • 全量的扫描新生代和老年代会不会很慢?肯定会,CMS 号称是停顿时间最短的GC,如此长的停顿时间肯定是不能接受的。 生代Eden区的内存使用量大于参数CMSScheduleRemarkEdenSizeThreshold 默认是2M,如果新生代的对象太少,就没有必要执行该阶段,直接执行重新标记阶段。

  • 如果新生代很多,超过了上述的阈值,如果在扫描新生代前进行一次Minor GC,那么在下一阶段remard的时候,新生代的对象会很少,从而节省很多时间。

  • CMS 有两个参数:

1
2
CMSScheduleRemarkEdenSizeThreshold
CMSScheduleRemarkEdenPenetration

默认值分别是2M、50%。两个参数组合起来的意思是预清理后,eden空间使用超过2M时启动可中断的并发预清理(CMS-concurrent-abortable-preclean),直到eden空间使用率达到50%时中断,进入remark阶段。

  • 如果能在可中止的预清理阶段发生一次 Minor GC,那就万事大吉、天下太平了。 这里有一个小问题,可终止的预清理要执行多长时间来保证发生一次Minor GC?答案是没法保证。道理很简单,因为垃圾回收是JVM自动调度的,什么时候进行GC我们控制不了。但此阶段总有一个执行时间吧?是的。

  • CMS提供了一个参数CMSMaxAbortablePrecleanTime ,默认为5S。只要到了5S,不管发没发生Minor GC,有没有到CMSScheduleRemardEdenPenetration都会中止此阶段,进入remark。如果在5S内还是没有执行Minor GC怎么办?CMS提供CMSScavengeBeforeRemark参数,使remark前强制进行一次Minor GC。

这样做利弊都有。好的一面是减少了remark阶段的停顿时间;坏的一面是Minor GC后紧跟着一个remark pause。如此一来,停顿时间也比较久。

我们来结合日志来看一下这个阶段
CMS日志如下:

1
2
3
4
5
6
7
8
9
10
11
7688.150: [CMS-concurrent-preclean-start]

7688.186: [CMS-concurrent-preclean: 0.034/0.035 secs]

7688.186: [CMS-concurrent-abortable-preclean-start]

7688.465: [GC 7688.465: [ParNew: 1040940K->1464K(1044544K), 0.0165840 secs] 1343593K->304365K(2093120K),

0.0167509 secs]7690.093: [CMS-concurrent-abortable-preclean: 1.012/1.907 secs] 7690.095: [GC[YG occupancy: 522484 K (1044544 K)]

7690.095: [Rescan (parallel) , 0.3665541 secs]7690.462: [weak refs processing, 0.0003850 secs] [1 CMS-remark: 302901K(1048576K)] 825385K(2093120K), 0.3670690 secs]

7688.186启动了可终止的预清理,在随后的三秒内启动了Minor GC,然后进入了Remark阶段.实际上为了减少remark阶段的STW时间

重新标记(FinalMarking)

这个阶段会导致第二次stop the word,该阶段的任务是完成标记整个年老代的所有的存活对象。
这个阶段,重新标记的内存范围是整个堆,包含_young_gen和_old_gen。为什么要扫描新生代呢,因为对于老年代中的对象,如果被新生代中的对象引用,那么就会被视为存活对象,即使新生代的对象已经不可达了,也会使用这些不可达的对象当做cms的“gc root”,来扫描老年代; 因此对于老年代来说,引用了老年代中对象的新生代的对象,也会被老年代视作“GC ROOTS”:当此阶段耗时较长的时候,可以加入参数-XX:+CMSScavengeBeforeRemark,在重新标记之前,先执行一次ygc,回收掉年轻带的对象无用的对象,并将对象放入幸存带或晋升到老年代,这样再进行年轻带扫描时,只需要扫描幸存区的对象即可,一般幸存带非常小,这大大减少了扫描时间。
由于之前的预处理阶段是与用户线程并发执行的,这时候可能年轻带的对象对老年代的引用已经发生了很多改变,这个时候,remark阶段要花很多时间处理这些改变,会导致很长stop the word,所以通常CMS尽量运行Final Remark阶段在年轻代是足够干净的时候。

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

并发清理 ( CMS-concurrent-sweep )

这个阶段的目的就是移除那些不用的对象,回收他们占用的空间并且为将来使用。注意这个阶段会产生新的垃圾,新的垃圾在此次GC无法清除,只能等到下次清理。这些垃圾有个专业名词:浮动垃圾。

并发重置 ( CMS-concurrent-reset )

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

CMS 调优

CMS JVM 参数

参数名 作用
-XX:+PrintCommandLineFlags 打印出启动参数行
-XX:+UseConcMarkSweepGC 参数指定使用CMS垃圾回收器
-XX:+UseCMSInitiatingOccupancyOnly 命令JVM不基于运行时收集的数据来启动CMS垃圾收集周期。而是,当该标志被开启时,JVM通过 CMSInitiatingOccupancyFraction 的值进行每一次CMS收集,而不仅仅是第一次。然而,请记住大多数情况下,JVM比我们自己能作出更好的垃圾收集决策。因此,只有当我们充足的理由(比如测试)并且对应用程序产生的对象的生命周期有深刻的认知时,才应该使用该标志。
-XX:CMSInitiatingOccupancyFraction=80 参数指定CMS垃圾回收器在老年代达到80%的时候开始工作,如果不指定那么默认的值为92%
-XX:+CMSClassUnloadingEnabled 开启永久带(jdk1.8以下版本)或元数据区(jdk1.8及其以上版本)收集,如果没有设置这个标志,一旦永久代或元数据区耗尽空间也会尝试进行垃圾回收,但是收集不会是并行的,而再一次进行Full GC
-XX:+UseParNewGC 使用cms时默认这个参数就是打开的,不需要配置,cms只回收老年代,年轻带只能配合Parallel New或Serial回收器;
-XX:+CMSParallelRemarkEnabled 减少Remark阶段暂停的时间,启用并行Remark,如果Remark阶段暂停时间长,可以启用这个参数 用于重新标记阶段是否采用多线程并行执行
-XX:+CMSScavengeBeforeRemark 若Remark阶段暂停时间太长,可以启用这个参数,在Remark执行之前,先做一次ygc。因为这个阶段,年轻带也是cms的gcroot,cms会扫描年轻带指向老年代对象的引用,如果年轻带有大量引用需要被扫描,会让Remark阶段耗时增加
-XX:CMSFullGCsBeforeCompaction=0
-XX:+UseCMSCompactAtFullCollection
两个参数是针对cms垃圾回收器碎片做优化的,CMS是不会移动内存的, 运行时间长了,会产生很多内存碎片, 导致没有一段连续区域可以存放大对象,出现promotion failedconcurrent mode failure, 导致fullgc,启用UseCMSCompactAtFullCollection 在FULL GC的时候, 对年老代的内存进行压缩。-XX:CMSFullGCsBeforeCompaction=0 则是代表多少次FGC后对老年代做压缩操作,默认值为0,代表每次都压缩, 把对象移动到内存的最左边,可能会影响性能,但是可以消除碎片,这两个错误会在后面的章节中详细介绍
-XX:+CMSConcurrentMTEnabled
-XX:ConcGCThreads=4
定义并发CMS过程运行时的线程数。比如value=4意味着CMS周期的所有阶段都以4个线程来执行。尽管更多的线程会加快并发CMS过程,但其也会带来额外的同步开销。因此,对于特定的应用程序,应该通过测试来判断增加CMS线程数是否真的能够带来性能的提升。如果未设置这个参数,JVM会根据并行收集器中的-XX:ParallelGCThreads参数的值来计算出默认的并行CMS线程数,此字段如果不清楚的情况下不要设置
ParallelGCThreads = (ncpus <=8 ? ncpus : 8+(ncpus-8)*5/8)(ncpus为cpu个数)
ConcGCThreads =(ParallelGCThreads + 3)/4
-XX:+ExplicitGCInvokesConcurrent
-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses
开启foreground CMS GC,CMS gc 有两种模式,background和foreground,正常的cms gc使用background模式,就是我们平时说的cms gc;当并发收集失败或者调用了System.gc()的时候,就会导致一次full gc,这个fullgc是不是cms回收,而是Serial单线程回收器,加入了参数-XX:+ExplicitGCInvokesConcurrent后,执行full gc的时候,就变成了CMS foreground gc,它是并行full gc,只会执行cms中stop the world阶段的操作,效率比单线程Serial full GC要高;需要注意的是它只会回收old,因为cms收集器是老年代收集器;而正常的Serial收集是包含整个堆的,加入了参数-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses,代表永久带也会被cms收集;
-XX:+CMSParallelInitialMarkEnabled 初始标记阶段是否采用多线程并行执行
- -XX:+PrintGCDetails 日志详细打印参数
- -XX:+PrintGCCause 日志详细打印参数
- -XX:+PrintGCTimeStamps 日志详细打印参数
- -XX:+PrintGCDateStamps 日志详细打印参数
- -Xloggc:…/logs/gc.log 日志详细打印参数

日志分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2017-07-27T10:42:56.291-0800: 6.963: [GC (CMS Final Remark)
[YG occupancy: 199103 K (306688 K)]
2017-07-27T10:42:56.291-0800: 6.963: [Rescan (parallel) , 0.0027865 secs]
2017-07-27T10:42:56.293-0800: 6.966: [weak refs processing, 0.0000397 secs]
2017-07-27T10:42:56.294-0800: 6.966: [class unloading, 0.0004163 secs]
2017-07-27T10:42:56.294-0800: 6.967: [scrub symbol table, 0.0006806 secs]
2017-07-27T10:42:56.295-0800: 6.967: [scrub string table, 0.0001862 secs]
[1 CMS-remark: 1569615K(1756416K)]
1768718K(2063104K),
0.0043575 secs]
[Times: user=0.01 sys=0.00, real=0.00 secs]
2017-07-27T10:42:56.291-0800: 6.963: [GC (CMS Final Remark(最终标记阶段,标记老年代中所有存活的对象,包括在此前的并发标记过程中创建/修改的引用))
[YG occupancy: 199103 K(当前年轻代的使用量) (306688 K(年轻代总大小))]
2017-07-27T10:42:56.291-0800: 6.963: [Rescan (parallel) , 0.0027865 secs](在程序暂停时重新进行扫描,以完成存活对象的标记。此时 Rescan 是并行执行的)
2017-07-27T10:42:56.293-0800: 6.966: [weak refs processing, 0.0000397 secs](第一个子阶段,处理弱引用)
2017-07-27T10:42:56.294-0800: 6.966: [class unloading, 0.0004163 secs](第二个子阶段,卸载不使用的类)
2017-07-27T10:42:56.294-0800: 6.967: [scrub symbol table, 0.0006806 secs]
2017-07-27T10:42:56.295-0800: 6.967: [scrub string table, 0.0001862 secs](最后一个子阶段,清理持有 class 级别 metadata 的符号表,以及内部化字符串对应的 string tables
[1 CMS-remark: 1569615K(1756416K)](此阶段完成后老年代的使用量和总容量)
1768718K(2063104K),(此阶段完成后整个堆内存的使用量和总容量)
0.0043575 secs]
[Times: user=0.01 sys=0.00, real=0.00 secs]

CMS gc 分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/第一步 初始标记 这一步会停顿
[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]

CMS 的处理流程讲完了,由于篇幅有些长,分一下章节,下次会围绕CMS 的后续问题 展开一些讨论 欢迎阅读。


参考:

垃圾回收统一理论
Introduction to Garbage Collection
R大:并发垃圾收集器(CMS)为什么没有采用标记-整理算法来实现?
R大:请教Weak Reference及其在HotSpot GC中的行为?
R大:请教Weak Reference及其在HotSpot GC中的行为?
Java垃圾回收浅析(2)-GC方式介绍



支付宝打赏 微信打赏

赞赏一下