CMS垃圾回收器
一、简介
Concurrent Mark Sweep
简称CMS
,是一种以获取最短回收停顿时间为目标的收集器,尤其重视服务的响应速度。
CMS
是老年代垃圾回收器,基于标记-清除算法实现,只回收老年代和永久代(jdk 1.8
开始为元数据区,需要设置CMSClassUnloadingEnabled
),一般配合perNew
使用。
CMS
是一种预处理垃圾回收器,它不能等到old
内存用尽时回收,需要在内存用尽前,完成回收操作,否则会导致并发回收失败;所以CMS
垃圾回收器开始执行回收操作,有一个触发阈值,默认是老年代或永久代达到92%
。
二、工作原理
CMS
是最常用的垃圾垃圾回收器之一,下面分析下CMS
垃圾回收器工作原理。
2.1 处理过程
- 初始标记(CMS-initial-mark),会导致
STW
; - 并发标记(CMS-concurrent-mark),与用户线程同时运行;
- 预清理(CMS-concurrent-preclean),与用户线程同时运行;
- 可被终止的预清理(CMS-concurrent-abortable-preclean),与用户线程同时运行;
- 重新标记(CMS-remark),会导致
STW
; - 并发清理(CMS-concurrent-sweep),与用户线程同时运行;
- 并发重置状态等待下次CMS的触发(CMS-concurrent-reset),与用户线程同时运行;
其中初始标记、并发标记、重新标记和并发清理这四个比较重要。
CMS
运行流程图如下所示:
下面抓取一下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]打印出启动参数行;
-
[2]参数指定使用
CMS
垃圾回收器; -
[3]、[4]参数指定
CMS
垃圾回收器在老年代达到80%
的时候开始工作,如果不指定那么默认的值为92%
; -
[5]开启永久代(
jdk 1.8
以下版本)或元数据区(jdk 1.8
及其以上版本)收集,如果没有设置这个标志,一旦永久代或元数据区耗尽空间也会尝试进行垃圾回收,但是收集不会是并行的,而再一次进行Full GC
; -
[6]使用
CMS
时默认这个参数就是打开的,不需要配置,CMS
只回收老年代,年轻代只能配合Parallel New
或Serial
回收器; -
[7]减少
Remark
阶段暂停的时间,启用并行Remark
,如果Remark
阶段暂停时间长,可以启用这个参数 -
[8]如果
Remark
阶段暂停时间太长,可以启用这个参数,在Remark
执行之前,先做一次ygc
。因为这个阶段,年轻代也是CMS
的gcroot
,CMS
会扫描年轻代指向老年代对象的引用,如果年轻代有大量引用需要被扫描,会让Remark
阶段耗时增加; -
[9]、[10]两个参数是针对
CMS
垃圾回收器碎片做优化的,CMS
是不会移动内存的,运行时间长了,会产生很多内存碎片,导致没有一段连续区域可以存放大对象,出现”promotion failed
”、”concurrent mode failure
”,导致fullgc
,启用UseCMSCompactAtFullCollection
在Full 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] -
[11]定义并发
CMS
过程运行时的线程数。比如value=4
意味着CMS
周期的所有阶段都以4
个线程来执行。尽管更多的线程会加快并发CMS
过程,但其也会带来额外的同步开销。因此,对于特定的应用程序,应该通过测试来判断增加CMS
线程数是否真的能够带来性能的提升。如果未设置这个参数,JVM
会根据并行收集器中的-XX:ParallelGCThreads
参数的值来计算出默认的并行CMS
线程数(ncpus
为cpu
个数):\(ParallelGCThreads = (ncpus <= 8 ? ncpus : 8 + (ncpus - 8)* 5/8)\),\(ConcGCThreads = (ParallelGCThreads + 3)/4\),这个参数一般不要自己设置,使用默认就好,除非发现默认的参数有调整的必要; -
[13]、[14]开启
foreground
CMS GC
,CMS gc
有两种模式,background
和foreground
,正常的CMS GC
使用background
模式,就是我们平时说的CMS GC
;当并发收集失败或者调用了System.gc()
的时候,就会导致一次Full GC
,这个Full GC
是不是CMS
回收,而是Serial
单线程回收器,加入了参数[12]后,执行Full GC
的时候,就变成了CMS foreground gc
,它是并行Full GC
,只会执行CMS
中stop the world
阶段的操作,效率比单线程Serial
full GC
要高;需要注意的是它只会回收old
,因为CMS
收集器是老年代收集器;而正常的Serial
收集是包含整个堆的,加入了参数[14],代表永久代也会被CMS
收集; -
[15]开启初始标记过程中的并行化,进一步提升初始化标记效率;
-
[16]、[17]、[18]、[19]、[20]是打印
gc
日志,其中[16]在jdk1.8
之后无需设置; -
[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
事件中的一次。这一步的作用是标记存活的对象,有两部分:
- 标记老年代中所有的
GC Roots
对象,如下图节点1; - 标记年轻代中活着的对象引用到的老年代的对象(指的是年轻代中还存活的引用类型对象,引用指向老年代中的对象)如下图节点2、3;
ps:为了加快此阶段处理速度,减少停顿时间,可以开启初始标记并行化,
-XX:+CMSParallelInitialMarkEnabled
,同时调大并行标记的线程数,线程数不要超过cpu
的核数。
2.3 并发标记
从“初始标记”阶段标记的对象开始找出所有存活的对象;
因为是并发运行的,在运行期间会发生新生代的对象晋升到老年代、或者是直接在老年代分配对象、或者更新老年代对象的引用关系等等,对于这些对象,都是需要进行重新标记的,否则有些对象就会被遗漏,发生漏标的情况。为了提高重新标记的效率,该阶段会把上述对象所在的Card
标识为Dirty
,后续只需扫描这些Dirty Card
的对象,避免扫描整个老年代;
并发标记阶段只负责将引用发生改变的Card
标记为Dirty
状态,不负责处理;
如下图所示,也就是节点1、2、3,最终找到了节点4和5。并发标记的特点是和应用程序线程同时运行。并不是老年代的所有存活对象都会被标记,因为标记的同时应用程序会改变一些对象的引用等。
这个阶段因为是并发的容易导致concurrent mode failure
。
2.4 预清理阶段
前一个阶段已经说明,不能标记出老年代全部的存活对象,是因为标记的同时应用程序会改变一些对象引用,这个阶段就是用来处理前一个阶段因为引用关系改变导致没有标记到的存活对象的,它会扫描所有标记为Direty
的Card
。
如下图所示,在并发清理阶段,节点3的引用指向了6;则会把节点3的card
标记为Dirty
;
最后将6标记为存活,如下图所示:
2.5 可终止的预处理
这个阶段尝试着去承担下一个阶段Final Remark
阶段足够多的工作。这个阶段持续的时间依赖好多的因素,由于这个阶段是重复的做相同的事情直到发生aboart
的条件(比如:重复的次数、多少量的工作、持续的时间等等)之一才会停止。
ps:此阶段最大持续时间为5秒,之所以可以持续5秒,另外一个原因也是为了期待这5秒内能够发生一次
ygc
,清理年轻代的引用,是的下个阶段的重新标记阶段,扫描年轻代指向老年代的引用的时间减少;
2.6 重新标记
这个阶段会导致第二次stop the word
,该阶段的任务是完成标记整个年老代的所有的存活对象。
这个阶段,重新标记的内存范围是整个堆,包含young_gen
和old_gen
。为什么要扫描新生代呢,因为对于老年代中的对象,如果被新生代中的对象引用,那么就会被视为存活对象,即使新生代的对象已经不可达了,也会使用这些不可达的对象当做CMS
的gc root
,来扫描老年代;因此对于老年代来说,引用了老年代中对象的新生代的对象,也会被老年代视作GC ROOTS
:当此阶段耗时较长的时候,可以加入参数-XX:+CMSScavengeBeforeRemark
,在重新标记之前,先执行一次ygc
,回收掉年轻代的对象无用的对象,并将对象放入幸存代或晋升到老年代,这样再进行年轻代扫描时,只需要扫描幸存区的对象即可,一般幸存代非常小,这大大减少了扫描时间。
由于之前的预处理阶段是与用户线程并发执行的,这时候可能年轻代的对象对老年代的引用已经发生了很多改变,这个时候,remark
阶段要花很多时间处理这些改变,会导致很长stop the word
,所以通常CMS
尽量运行Final Remark
阶段在年轻代是足够干净的时候。
另外,还可以开启并行收集:-XX:+CMSParallelRemarkEnabled
。
2.7 并发清理
通过以上5个阶段的标记,老年代所有存活的对象已经被标记并且现在要通过Garbage Collector
采用清扫的方式回收那些不能用的对象了。
这个阶段主要是清除那些没有标记的对象并且回收空间;
由于CMS
并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS
无法在当次收集中处理掉它们,只好留待下一次GC
时再清理掉。这一部分垃圾就称为“浮动垃圾”。
2.8 并发重置
这个阶段并发执行,重新设置CMS
算法内部的数据结构,准备下一个CMS
生命周期的使用。
CMS
并发GC
不是Full GC
。
HotSpot VM
里对concurrent collection
和full collection
有明确的区分。所有带有full collection
字样的VM
参数都是跟真正的Full GC
相关,而跟CMS
并发GC
无关的,CMS
收集算法只是清理老年代。
三、三色标记算法
3.1 基本算法
要找出存活对象,根据可达性分析,从GC Roots
开始进行遍历访问,可达的则为存活对象。将遍历对象图过程中遇到的对象,按“是否访问过”这个条件标记成以下三种颜色:
- 白色:未被标记的对象
- 黑色:本对象和成员变量都被标记
- 灰色:本对象已被标记,成员对象未被标记。当全部标记后,会转换为黑色
假设现在有白、灰、黑三个集合(表示当前对象的颜色),其遍历访问过程为:
- 初始时,所有对象都在【白色集合】中;
- 将
GC Roots
直接引用到的对象挪到【灰色集合】中; - 从灰色集合中获取对象:
3.1. 将本对象引用到的其他对象全部挪到【灰色集合】中;
3.2. 将本对象挪到【黑色集合】里面。 - 重复步骤3,直至【灰色集合】为空时结束。
- 结束后,仍在【白色集合】的对象即为
GC Roots
不可达,可以进行回收。
注:如果标记结束后对象仍为白色,意味着已经“找不到”该对象在哪了,不可能会再被重新引用。
当Stop The World
(STW
)时,对象间的引用是不会发生变化的,可以轻松完成标记。而当需要支持并发标记时,即标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生。
3.2 多标-浮动垃圾
在并发标记过程中,如果由于方法运行结束导致部分局部变量(GC Root
)被销毁,这个GC Root
引用的对象之前又被扫描过(被标记为非垃圾对象),那么本轮GC
不会回收这部分内存。这部分本应该回收但是没有回收到的内存,被称之为"浮动垃圾"。浮动垃圾并不会影响垃圾回收的正确性,只是需要等到下一轮垃圾回收中才被清除。
另外,针对并发标记(还有并发清理)开始后产生的新对象,通常的做法是直接全部当成黑色,本轮不会进行清除。这部分对象期间可能也会变为垃圾,这也算是浮动垃圾的一部分。
3.3 漏标-读写屏障
多标无伤大雅,但是漏标就会造成重大错误了,会导致应该存货的对象被当成垃圾回收掉,这是绝对不允许的,下面举例说明漏标的情况:
(1) 最开始A
指向B
,B
指向D
,但是在A
完成标记后,标记到了B
还没标记D
时,B
指向D
的引用消失,而A
又指向了D
,此时因为A
标记为黑色表示所有子对象都被标记,所以D
不会被检测到,从而导致漏标。如下图:
![](https://img2022.cnblogs.com/blog/2745643/202206/2745643-20220611115919457-1433674708.png)
为了解决上诉问题,CMS
给出了自己的解决方案,就是一个对象A
的引用指向了另一个对象D
,那么将这个对象A
重新标记为灰色(这种方式也有bug
,见下文),如下:
![](https://img2022.cnblogs.com/blog/2745643/202206/2745643-20220610235241792-1625779491.png)
(2) 由CMS
的Incremental Update
而产生的漏标问题。如下图:
![](https://img2022.cnblogs.com/blog/2745643/202206/2745643-20220610235033422-1810671440.png)
要出现上诉情况分为以下几步:
(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阶段停顿
一般CMS
的GC
耗时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
时做压缩,会依赖几个条件。其中:
UseCMSCompactAtFullCollection
与CMSFullGCsBeforeCompaction
是搭配使用的;前者目前默认就是true
了,也就是关键在后者上。- 用户调用了
System.gc()
,而且DisableExplicitGC
没有开启。 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
才做一次压缩(而不是每10
次CMS
并发GC
就做一次压缩,目前VM
里没有这样的参数)。这会使Full GC
更少做压缩,也就更容易使CMS
的old 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
)。
早提升的原因
Survivor
空间太小,容纳不下全部的运行时短生命周期的对象,如果是这个原因,可以尝试将Survivor
调大,否则端生命周期的对象提升过快,导致老年代很快就被占满,从而引起频繁的Full GC
;- 对象太大,
Survivor
和Eden
没有足够大的空间来存放这些大象;
提升失败原因
当提升的时候,发现老年代也没有足够的连续空间来容纳该对象。
为什么是没有足够的连续空间而不是空闲空间呢?
老年代容纳不下提升的对象有两种情况:
- 老年代空闲空间不够用了;
- 老年代虽然空闲空间很多,但是碎片太多,没有连续的空闲空间存放该对象;
解决方法
- 如果是因为内存碎片导致的大对象提升失败,
CMS
需要进行空间整理压缩; - 如果是因为提升过快导致的,说明
Survivor
空闲空间不足,那么可以尝试调大Survivor
; - 如果是因为老年代空间不够导致的,尝试将
CMS
触发的阈值调低;
4.5 其它导致回收停顿时间变长原因
linux
使用了swap
,内存换入换出(vmstat
),尤其是开启了大内存页的时候,因为swap
只支持4k
的内存页,大内存页的大小为2M
,大内存页在swap
的交换的时候需要先将swap
中4k
内存页合并成一个大内存页再放入内存或将大内存页切分为4k
的内存页放入swap
,合并和切分的操作会导致操作系统占用cup
飙高,用户cpu
占用反而很低;
除了swap
交换外,网络io
(netstat
)、磁盘I/O
(iostat
)在GC
过程中发生会使GC
时间变长。
如果是以上原因,就要去查看gc
日志中的Times
耗时:
[Times: user=0.00 sys=0.00, real=0.00 secs]
user
是用户线程占用的时间,sys
是系统线程占用的时间,如果是io
导致的问题,会有两种情况:
1.user
与sys
时间都非常小,但是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
上运行,而不需要进一步思考。