JVM 垃圾回收器 — CMS
垃圾回收器 CMS
CMS(Concurrent Mark-Sweep)是以牺牲吞吐量为代价来获得最短回收停顿时间的老年代垃圾回收器,基于“标记-清除”算法的“多线程”垃圾回收。对于要求服务器响应速度的交互式应用,这种垃圾回收器非常适合。在启动 JVM 参数加上 -XX:+UseConcMarkSweepGC
启用 CMS 作为垃圾回收器。
垃圾回收过程
- 初始标记(CMS-initial-mark,STW)
该阶段进行“可达性分析”,需要暂停正在执行的用户线程,从 GC Roots 开始,只扫描到能够和 GC Roots 直接关联的对象,并作标记,所以这个过程虽然暂停了整个 JVM,但是很快就完成了。
-
并发标记(CMS-concurrent-mark)
该阶段进行 GC ROOT TRACING,此时恢复用户线程运行,在初始标记的基础上继续向下追溯标记,所有可到达的对象都在本阶段中标记。应用程序的线程和并发标记的线程并发执行。
-
并发预清理(CMS-concurrent-preclean)
该阶段查找在执行“并发标记”阶段新进入老年代的对象(可能会有一些对象从新生代晋升到老年代, 或者有一些对象被分配到老年代),以及在“并发标记”阶段被修改的对象。
因为上一阶段的时间较长,会有一些 changed object。通过重新扫描,减少下一个阶段“重新标记”的工作,因为下一个阶段会 STW。
并发预清理阶段仍然是并发的。利用到 Card table 查找变化的对象。
- 预清理中止(CMS-concurrent-abortable-preclean)
执行可中止预清理
- 预清理中止(CMS-concurrent-abortable-preclean)
-
重新标记(CMS-remark,STW)
该阶段重新扫描堆中的对象,进行“可达性分析”,需要暂停正在执行的用户线程。
-
并发清理(CMS-concurrent-sweep)
该阶段清理垃圾对象,此时恢复用户线程运行。
-
并发重置(CMS-Concurrent-reset)
该阶段重置 CMS 收集器的数据结构,等待下一次垃圾回收。
CMS 垃圾回收日志
分析一次完整的 CMS 垃圾回收日志
========初始标记============
[GC [1 CMS-initial-mark: 1996330K(2124800K)] 2089808K(3270400K), 0.0667700 secs] [Times: user=0.06 sys=0.00, real=0.07 secs]
========依次表示标记前后old区的所有对象占内存大小[1996330K(2124800K)],整个JavaHeap(不包括perm)所有对象占内存总的大小[2089808K(3270400K)]。============
Total time for which application threads were stopped: 0.0674020 seconds
========并发标记=========
[CMS-concurrent-mark-start]
Total time for which application threads were stopped: 0.0010340 seconds
[CMS-concurrent-mark: 1.931/1.932 secs] [Times: user=6.94 sys=0.30, real=1.93 secs]
========并发预清理======
[CMS-concurrent-preclean-start]
[CMS-concurrent-preclean: 0.117/0.137 secs] [Times: user=0.15 sys=0.06, real=0.13 secs]
[CMS-concurrent-abortable-preclean-start]CMS: abort preclean due to time
[CMS-concurrent-abortable-preclean: 5.140/5.190 secs] [Times: user=7.10 sys=1.45, real=5.19 secs]
=========重新标记=================
[GC[YG occupancy: 270973 K (1145600 K)]
[Rescan (parallel) , 0.2171280 secs]
[weak refs processing, 0.0000700 secs]
[scrub string table, 0.0016420 secs] [1 CMS-remark: 1996330K(2124800K)] 2267303K(3270400K), 0.2190720 secs] [Times: user=2.13 sys=0.00, real=0.22 secs]
Total time for which application threads were stopped: 0.2232820 seconds
==========并发清除======
[CMS-concurrent-sweep-start]
[CMS-concurrent-sweep: 0.948/0.948 secs] [Times: user=1.53 sys=0.18, real=0.95 secs]
==========并发重设状态==
[CMS-concurrent-reset-start]
[CMS-concurrent-reset: 0.005/0.005 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
其他可能出现的日志
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]
==========发生 promotion failed
0.195: [GC 0.195: [ParNew: 2986K->2986K(8128K), 0.0000083 secs]
0.195: [CMS0.212: [CMS-concurrent-preclean: 0.011/0.031 secs] [Times: user=0.03 sys=0.02, real=0.03 secs]
(concurrent mode failure): 56046K->138K(57344K), 0.0271519 secs] 59032K->138K(65472K), [CMS Perm : 2079K->2078K(12288K)], 0.0273119 secs] [Times: user=0.03 sys=0.00, real=0.03 secs]
==========发生 concurrent mode failure
CMS 的相关概念
什么时候会触发 CMS GC
- 老年代或已经使用的空间达到设定的百分比时(
-XX:CMSInitiatingOccupancyFraction=-1
,默认是在老年代占满 92% 的时候开始进行 CMS 收集) -
JVM 自动触发(JVM 的动态策略,也就是悲观策略),基于之前 GC 的频率以及老年代的增长趋势,在老年代空间使用完之前,执行垃圾回收。如果不希望 JVM 自行决定,可以通过
-XX: UseCMSInitiatingOccupancyOnly=true
来设置。
值的注意的是 CMS GC 不等于 Full GC,很多人会认为 CMS GC 肯定会引发 Minor GC。CMS 是针对老年代的GC策略,原则上不会去清理新生代,只有设置 CMSScavengeBeforeRemark 优化时,或者是 concurrent mode failure 的时候才会去做 Minor GC 以及 Full GC。
什么时候会触发 Minor GC
- 新生代回收器触发,Eden 区域满了,或者新创建的对象大小大于 Eden 所剩空间
-
CMS 设置了
-XX:+CMSScavengeBeforeRemark
参数,在 CMS Remark 之前会先做一次 Minor GC 来清理新生代,加速之后的 Remark 的速度 -
Full GC 的时候会触发 Minor GC,CMS 出现 concurrent mode failure 的时候会触发 Full GC
什么时候会触发 Full GC
-
老年代空间不足
-
永久代空间不足,可以让 CMS 清理永久代的空间。设置
-XX:+CMSClassUnloadingEnabled
-
CMS GC 时出现 promotion failed 和 concurrent mode failure
-
统计得到的 Minor GC 晋升到老年代的平均大小大于老年代的剩余空间
-
主动触发Full GC,执行
jmap -histo:live [pid]
来避免碎片问题,或者System.gc()
,可以设置-XX:+DisableExplicitGC
来禁止调用System.gc()
什么是 promotion failure
发生在新生代回收阶段,Minor GC 后存活的对象晋升到老年代时有两种情况会触发 Full GC(老年代会为新生代对象的晋升提供担保):
- 之前每次晋升的对象的平均大小 > 老年代剩余空间,是基于历史平均值
-
Minor GC 后存活的对象 > 老年代剩余空间,基于下一次可能要晋升的最大值
处理方式:可以适当增大 Survivor 空间来减少这种情况
什么是 concurrent mode failure
发生在老年代回收阶段,主要是并行阶段,用户线程创建的大对象直接晋升到老年代,而老年代没有足够空间(准确说是没有足够的连续可用空间,因为 CMS 会产生很多碎片),必须先暂停用户线程(STW),执行带有压缩的 FullGC 清理。
处理方式:
- 可以增大老年代空间
- 可以减少
-XX:CMSInitiatingOccupancyFraction=n
(老年代触发垃圾回收的阈值) 预留一部分老年代空间,但是相应的会增加老年代回收频率 -
可以设置 CMS 在进行n次的 Full GC 后,进行一次标记整理算法:
-XX:UseCMSCompactAtFullCollection -XX:CMSFullGCBeforeCompaction=5
为什么 CMS 会有内存碎片
因为 CMS 是采用的“标记-清除”算法,所以运行时间长会产生大量碎片,导致 GC 变慢。
-XX:UseCMSCompactAtFullCollection
开启每次执行 Full GC 的时候会进行整理(默认是开启的)
-XX:CMSFullGCsBeforeCompaction=n
指定多少次 Full GC 之后来执行整理(默认是0,即每一次)
为什么 CMS 两次标记时要 STW
当虚拟机完成两次标记后,便确认了可以回收的对象。但是,垃圾回收并不会阻塞用户线程,所以问题就会出现,当 GC 线程标记好了一个对象的时候,此时用户线程又将该对象重新加入了“引用关系”中,回收的时候就会回收这个不该回收的对象。
http://stackoverflow.com/questions/29846041/why-remark-phase-is-needed-on-concurrent-gc
void swap(Object[] a, int i, int j) {
Object tmp = a[i];
a[i] = a[j];
// Now the original reference a[i] is in a register only.
a[j] = tmp;
}
总结 CMS 的缺陷
对 CPU 资源敏感
CMS 是并发垃圾回收器,并发意味着会有多线程抢占 CPU 资源,即 GC 线程与用户线程抢占 CPU 资源。这可能会造成用户线程执行效率下降。
CMS 默认的回收线程数是 (CPU个数+3)/4。当 CPU 大于4个时,回收线程占用25%的 CPU 资源,用户线程占用 75% 的 CPU 资源,是可以接受的。但是如果 CPU 资源很少,比如只有两个的时候,按照上面的公式,CMS 会启动1个 GC 线程。相当于 GC 线程占用了 50% 的CPU 资源,这就可能导致用户程序的执行速度降低过多。
浮动垃圾
并发清理阶段用户线程还在运行,这段时间就可能产生新的垃圾,新的垃圾在此次 GC 无法清除,只能等到下次清理。这些垃圾叫做浮动垃圾。
垃圾回收降级
由于垃圾回收阶段用户线程仍在执行,必需预留出内存空间给用户线程使用。因此不能像其他回收器那样,等到老年代满了再进行 GC。
CMS 提供了 -XX:CMSInitiatingOccupancyFraction=n
参数来设置老年代空间使用百分比,达到百分比就进行垃圾回收。这个参数默认是92%,参数选择需要看具体的应用场景。
- 设置的太小会导致频繁的 CMS GC,产生大量的停顿;
-
设置的太高会导致垃圾回收器降级
假设设置为 99%,还剩 1% 的老年代空间可以使用。在并发清理阶段,若用户线程需要使用的空间大于1%,就会产生 Concurrent Mode Failure 错误,即并发模式失败。JVM 会使用 Serial Old 收集器重新对老年代进行垃圾回收,停顿时间变得更长。
空间碎片
使用标记-清除算法可能造成大量的空间碎片。空间碎片过多,就会给大对象分配带来麻烦。上面介绍过,通过两个参数可以指定在 FullGC 间隔执行带压缩的 FullGC。