图文并茂看CMS内存回收

CMS 日志

2020-08-26T12:52:41.268+0800: 463.575: Total time for which application threads were stopped: 0.4222317 seconds, Stopping threads took: 0.0001890 seconds
2020-08-26T12:52:41.268+0800: 463.575: Application time: 0.0000643 seconds

// 初始标记(Initial Mark)
2020-08-26T12:52:41.271+0800: 463.578: [GC (CMS Initial Mark) [1 CMS-initial-mark: 1777676K(3145728K)] 1987618K(5033216K), 0.0327958 secs] [Times: user=0.12 sys=0.00, real=0.04 secs]


// 并发标记(Concurrent Mark)
2020-08-26T12:52:41.304+0800: 463.611: [CMS-concurrent-mark-start]
2020-08-26T12:52:42.050+0800: 464.356: [CMS-concurrent-mark: 0.746/0.746 secs] [Times: user=2.61 sys=0.21, real=0.74 secs]

// 并发预清理(Concurrent Preclean)
2020-08-26T12:52:42.050+0800: 464.356: [CMS-concurrent-preclean-start]
2020-08-26T12:52:42.171+0800: 464.477: [CMS-concurrent-preclean: 0.056/0.121 secs] [Times: user=0.47 sys=0.05, real=0.12 secs]

// 并发可取消的预清理(Concurrent Abortable Preclean)
2020-08-26T12:52:42.171+0800: 464.477: [CMS-concurrent-abortable-preclean-start]
2020-08-26T12:52:43.753+0800: 466.059: [CMS-concurrent-abortable-preclean: 1.128/1.582 secs] [Times: user=4.86 sys=0.41, real=1.59 secs]

// 最终标记(Final Remark)
2020-08-26T12:52:43.756+0800: 466.063: [GC (CMS Final Remark) [YG occupancy: 1414648 K (1887488 K)]466.063: [Rescan (parallel) , 0.2583497 secs]466.321:[weak refs processing, 0.0005512 secs]466.322: [class unloading, 0.0514608 secs]466.373: [scrub symbol table, 0.0122328 secs]466.385: [scrub string table, 0.0017416 secs][1 CMS-remark: 1974197K(3145728K)] 3388846K(5033216K), 0.3265593 secs] [Times: user=0.83 sys=0.00, real=0.33 secs]

// 并发清除(Concurrent Sweep)
2020-08-26T12:52:44.083+0800: 466.389: [CMS-concurrent-sweep-start]
2020-08-26T12:52:48.578+0800: 470.885: [CMS-concurrent-sweep: 3.224/4.495 secs] [Times: user=14.43 sys=1.19, real=4.49 secs]

// 并发重置(Concurrent Reset)
2020-08-26T12:52:48.638+0800: 470.945: [CMS-concurrent-reset-start]
2020-08-26T12:52:48.647+0800: 470.953: [CMS-concurrent-reset: 0.008/0.008 secs] [Times: user=0.06 sys=0.01, real=0.01 secs]

CMS 过程分析

在这里插入图片描述

三色标记

首先我们知道对象在标记过程中,根据标记情况,分成三类:

  1. 黑色对象,表示自身被标记,内部引用都被处理;
  2. 白色对象,表示自身未被标记;
  3. 灰色对象,表示自身被标记,但内部引用未被处理;
    在这里插入图片描述

初始状态

假设发生Garbage Collect时,Java堆的对象分布如下:
在这里插入图片描述

阶段 1: 初始标记(Initial Mark)

这一阶段主要是定位GC root,并进行可达性分析定位GC root直接关联的对象,注意是直接关联,以及由新生代中存活对象所引用的对象,此阶段是STW的。

这个过程是支持多线程的(JDK7之前单线程,JDK8之后并行,可通过参数CMSParallelInitialMarkEnabled调整),主要分分为两步:

  1. 标记GC Roots可达的老年代对象;
  2. 遍历新生代对象,标记可达的老年代对象;
    该过程结束后,对象分布如下:
    在这里插入图片描述

阶段 2: 并发标记(Concurrent Mark)

这一阶段是进行GC root tracing阶段,和用户线程并发执行,此时在第一阶段被暂停的用户线程在该阶段重新运行。在这阶段中,从上一阶段被标记的对象出发,递归标记所有可达的对象。可以理解为递归标记标记间接关联的对象。

因为该阶段并发执行的,在运行期间可能发生新生代的对象晋升到老年代、或者是直接在老年代分配对象、或者更新老年代对象的引用关系等等,对于这些对象,都是需要进行重新标记的,否则有些对象就会被遗漏,发生漏标的情况。

为了提高重新标记的效率,该阶段会把上述对象所在的Card标识为Dirty,后续只需扫描这些Dirty Card的对象,避免扫描整个老年代。

在这里插入图片描述

阶段 3: 并发预清理(Concurrent Preclean)

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

  1. 处理新生代已经发现的引用,比如在并发阶段,在Eden区中分配了一个A对象,A对象引用了一个老年代对象B(这个B之前没有被标记),在这个阶段就会标记对象B为活跃对象。
  2. 在并发标记阶段,如果老年代中有对象内部引用发生变化,会把所在的Card标记为Dirty(其实这里并非使用CardTable,而是一个类似的数据结构,叫ModUnionTalble),通过扫描这些Table,重新标记那些在并发标记阶段引用被更新的对象(晋升到老年代的对象、原本就在老年代的对象)

这一步所做的工作还是标记。CMS是以获取最短暂停时间为目的的GC,而在第五步重标记需要STW,因此重标记的工作尽可能多的在并发阶段完成来减少STW的时间。

此阶段GC线程和应用线程也是并发执行,因为阶段2是与应用线程并发执行,可能有些引用关系已经发生改变。
通过卡片标记(Card Marking),提前把老年代空间逻辑划分为相等大小的区域(Card),如果引用关系发生改变,JVM会将发生改变的区域标记位“脏区”(Dirty Card),然后在本阶段,这些脏区会被找出来,刷新引用关系,清除“脏区”标记

阶段 4: 并发可取消的预清理(Concurrent Abortable Preclean)

该阶段发生的前提是,新生代Eden区的内存使用量大于参数CMSScheduleRemarkEdenSizeThreshold( 默认是2M),如果新生代的对象太少,就没有必要执行该阶段,直接执行重新标记阶段。

为什么需要这个阶段,存在的价值是什么?

因为CMS GC的终极目标是降低垃圾回收时的暂停时间,所以在该阶段要尽最大的努力去处理那些在并发阶段被应用线程更新的老年代对象,这样在暂停的重新标记阶段就可以少处理一些,暂停时间也会相应的降低。

在该阶段,主要循环的做两件事:

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

当然了,这个逻辑不会一直循环下去,打断这个循环的条件有三个:

  1. 可以设置最多循环的次数 CMSMaxAbortablePrecleanLoops,默认是0,意思没有循环次数的限制。
  2. 如果执行这个逻辑的时间达到了阈值CMSMaxAbortablePrecleanTime,默认是5s,会退出循环。
  3. 如果新生代Eden区的内存使用率达到了阈值CMSScheduleRemarkEdenPenetration,默认50%,会退出循环。(这个条件能够成立的前提是,在进行Precleaning时,Eden区的使用率小于十分之一)。

阶段 5: 最终标记(Final Remark)

该阶段并发执行,在之前的并行阶段,可能产生新的引用关系如下:

  1. 老年代的新对象被GC Roots引用
  2. 老年代的未标记对象被新生代对象引用
  3. 老年代已标记的对象增加新引用指向老年代其它对象
  4. 新生代对象指向老年代引用被删除
  5. 也许还有其它情况…

上述对象中可能有一些已经在Precleaning阶段和AbortablePreclean阶段被处理过,但总存在没来得及处理的,所以还有进行如下的处理:

  1. 遍历新生代对象,重新标记
  2. 根据GC Roots,重新标记
  3. 遍历老年代的Dirty Card,重新标记,这里的Dirty Card大部分已经在preclean阶段处理过

有了前面的基础,这个阶段的工作量被大大减轻,停顿时间因此也会减少。

阶段 6: 并发清除(Concurrent Sweep)

此阶段与应用程序并发执行,不需要STW停顿,根据标记结果清除垃圾对象

阶段 7: 并发重置(Concurrent Reset)

此阶段与应用程序并发执行,重置CMS算法相关的内部数据, 为下一次GC循环做准备。

CMS并发预清理阶段剖析

大家知道在老年代通过GC ROOT TRANVCING可达的对象就是活的对象。现在考虑下面一种情况:
在这里插入图片描述
这张图中老年代中的Current obj被新生代的对象引用,在这种情况下这个对象也是活着的,所以在这种情况下,CMS必须要扫描新生代才能完整的标记处活着的对象,这就是为什么CMS作为老年代垃圾器,仍然要扫描新生代的原因。接下来具体分析一下在重标记阶段的GC日志,相关日志如下:

// 最终标记(Final Remark)
2020-08-26T12:52:43.756+0800: 466.063: [GC (CMS Final Remark) [YG occupancy: 1414648 K (1887488 K)]466.063: [Rescan (parallel) , 0.2583497 secs]466.321:[weak refs processing, 0.0005512 secs]466.322: [class unloading, 0.0514608 secs]466.373: [scrub symbol table, 0.0122328 secs]466.385: [scrub string table, 0.0017416 secs][1 CMS-remark: 1974197K(3145728K)] 3388846K(5033216K), 0.3265593 secs] [Times: user=0.83 sys=0.00, real=0.33 secs]

通过日志可以看到remark阶段会再分为多个阶段,首先是Rescan (parallel),这个阶段完成存活对象的标记工作,同时会扫描新生代和老年代的对象,是并行进行的。接下来会继续进行weak refs processing,class unloading,scrub symbol table,scrub string table的处理工作。Rescan子阶段会全量扫描新生代和老年代的对象,这样肯定会特别的慢。所以怎么能够快速识别新生代和老年代活着的对象呢?需要分别从新生代和老年代的角度去分析。

新生代

新生代垃圾回收完以后剩下的对象全是活着的,并且活着的对象很少。所以如果在Rescan前进行一次minorGC,情况是不是就会好很多。

// 并发预清理(Concurrent Preclean)
2020-08-26T12:52:42.050+0800: 464.356: [CMS-concurrent-preclean-start]
2020-08-26T12:52:42.171+0800: 464.477: [CMS-concurrent-preclean: 0.056/0.121 secs] [Times: user=0.47 sys=0.05, real=0.12 secs]

// 并发可取消的预清理(Concurrent Abortable Preclean)
2020-08-26T12:52:42.171+0800: 464.477: [CMS-concurrent-abortable-preclean-start]
2020-08-26T12:52:43.753+0800: 466.059: [CMS-concurrent-abortable-preclean: 1.128/1.582 secs] [Times: user=4.86 sys=0.41, real=1.59 secs]

这段日志对应的是CMS垃圾回收的第三/四阶段并发预清理阶段,通过日志可以看到在CMS-concurrent-preclean-start后又进行了CMS-concurrent-abortable-preclean-start。

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

如果能在可中止的预清理阶段发生一次Minor GC,那就万事大吉、天下太平了。

现在有一个问题concurrent-abortable-preclea阶段会执行一次YGC,但是concurrent-abortable-preclean需要执行多久来保证发生一次MinorGC呢。

答案是没法保证。道理很简单,因为垃圾回收是JVM自动调度的,什么时候进行GC我们控制不了。

但此阶段总有一个执行时间吧?是的。

CMS提供了一个参数CMSMaxAbortablePrecleanTime ,默认为5S。

只要到了5S,不管发没发生Minor GC,有没有到CMSScheduleRemardEdenPenetration都会中止此阶段,进入remark。

如果在5S内还是没有执行Minor GC怎么办?

CMS提供CMSScavengeBeforeRemark参数,使remark前强制进行一次Minor GC。这样做利弊都有。好的一面是减少了remark阶段的停顿时间;坏的一面是Minor GC后紧跟着一个remark pause。如此一来,停顿时间也比较久。

日志如下:

// 并发可取消的预清理(Concurrent Abortable Preclean) 开始
2020-08-26T12:52:42.171+0800: 464.477: [CMS-concurrent-abortable-preclean-start]
2020-08-26T12:52:42.338+0800: 464.645: Application time: 1.0343203 seconds
2020-08-26T12:52:42.341+0800: 464.648: Total time for which application threads were stopped: 0.0029816 seconds, Stopping threads took: 0.0003655 seconds
2020-08-26T12:52:42.553+0800: 464.860: Application time: 0.2120264 seconds

// YoungGC
2020-08-26T12:52:42.556+0800: 464.863: [GC (Allocation Failure) 464.863: [ParNew
Desired survivor size 107347968 bytes, new threshold 1 (max 6)
- age   1:  202959736 bytes,  202959736 total
: 1887488K->209664K(1887488K), 0.4412233 secs] 3665164K->2183861K(5033216K), 0.4415774 secs] [Times: user=1.22 sys=0.00, real=0.44 secs]

// 并发可取消的预清理(Concurrent Abortable Preclean) 结束
2020-08-26T12:52:43.753+0800: 466.059: [CMS-concurrent-abortable-preclean: 1.128/1.582 secs] [Times: user=4.86 sys=0.41, real=1.59 secs]

// 最终标记(Final Remark)
2020-08-26T12:52:43.756+0800: 466.063: [GC (CMS Final Remark) [YG occupancy: 1414648 K (1887488 K)]466.063: [Rescan (parallel) , 0.2583497 secs]466.321:[weak refs processing, 0.0005512 secs]466.322: [class unloading, 0.0514608 secs]466.373: [scrub symbol table, 0.0122328 secs]466.385: [scrub string table, 0.0017416 secs][1 CMS-remark: 1974197K(3145728K)] 3388846K(5033216K), 0.3265593 secs] [Times: user=0.83 sys=0.00, real=0.33 secs]

464.477启动了可终止的预清理,在随后的进行了Minor GC,然后进入了Remark阶段.

实际上为了减少remark阶段的STW时间,预清理阶段会尽可能多做一些事情来减少remark停顿时间。

remark的rescan阶段是多线程的,为了便于多线程扫描新生代,预清理阶段会将新生代分块。

每个块中存放着多个对象,这样remark阶段就不需要从头开始识别每个对象的起始位置。

多个线程的职责就很明确了,把分块分配给多个线程,很快就扫描完。

遗憾的是,这种办法仍然是建立在发生了Minor GC的条件下。

如果没有发生Minor GC,top(下一个可以分配的地址空间)以下的所有空间被认为是一个块(这个块包含了新生代大部分内容)。

这种块对于remark阶段并不会起到多少作用,因此并行效率也会降低。

老年代

老年代中存在一个叫做card table的数据结构,其核心就是一个数组,每个数组的位置存的是一个byte。CMS将老年代的空间分成大小为512byte的块,card table中的每个元素对应一个块。并发标记时,如果某个对象的引用发生了变化,就标记该对象所在的块为dirty card。并发预清理阶段就会重新扫描该块,将该对象引用的对象标识为可达。下面通过图示说明在并发标记时对象的状态如下:
在这里插入图片描述
在并发标记的同时current obj的引用发生了变化:
在这里插入图片描述
current obj所在的块被标记为了dirty card。随后到了预清理阶段,该阶段的主要任务就是标记在并发阶段被修改了的对象。在这阶段中通过current obj变得可达的对象也被标记了。同时dirty card标志也被清除。
在这里插入图片描述
还记得前面提到的那个问题么?进行Minor GC时,如果有老年代引用新生代,怎么识别?

(有研究表明,在所有的引用中,老年代引用新生代这种场景不足1%.原因大家可以自己分析下)

当有老年代引用新生代,对应的card table被标识为相应的值(card table中是一个byte,有八位,约定好每一位的含义就可区分哪个是引用新生代,哪个是并发标记阶段修改过的)。

所以,Minor GC通过扫描card table就可以很快的识别老年代引用新生代。

这里点一下,hotspot 虚拟机使用字节码解释器、JIT编译器、 write barrier维护 card table。

当字节码解释器或者JIT编译器更新了引用,就会触发write barrier操作card table.

再点一下,由于card table的存在,当老年代空间很大时会发生什么?(这里大家可以自由发挥想象)

至此,预清理阶段的工作讲完。

ParNew + CMS最佳实践

-server
-Xms<size> # 4c8g-6g、8c16g-12g、16c32g-28g
-Xmx<size> # 4c8g-6g、8c16g-12g、16c32g-28g
-Xss<size> # 512k

-Xloggc:<path>
-XX:+PrintGC
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+PrintGCTimeStamps

-XX:+HeapDumpOnOutOfMemoryError
-XX:+AlwaysPreTouch
-XX:HeapDumpPath=<path>
-XX:ErrorFile=<path>

-XX:PermSize=<size> (<= JDK7) # 512m
-XX:MaxPermSize=<size> (<= JDK7) # 512m
-XX:MetaspaceSize=<size> (>= JDK8) # 512m
-XX:MaxMetaspaceSize=<size> (>= JDK8) # 512m

-XX:NewRatio=<int> # Old:New 2
-XX:SurvivorRatio=<int> # Edon:1Survivor 8

-XX:+UseParNewGC
-XX:+UseConcMarkSweepGC
-XX:+CMSClassUnloadingEnabled
-XX:+UseCMSInitiatingOccupancyOnly
-XX:+UseCMSCompactAtFullCollection
-XX:CMSInitiatingOccupancyFraction=<int> # 75
-XX:CMSFullGCsBeforeCompaction=<int> # 5
-XX:ConcGCThreads=<int> # cores

参考:
https://www.cnblogs.com/littleLord/p/5380624.html
https://www.jianshu.com/p/2a1b2f17d3e4

相关推荐
©️2020 CSDN 皮肤主题: Age of Ai 设计师:meimeiellie 返回首页