1 CMS(Concurrent Mark Sweep)收集器
顾名思义,CMS收集器是以并发标记的方式去扫除JVM堆空间内的垃圾对象,其设计的目标是让GC操作的暂停时间更短,因为是并行操作,所以GC操作会与应用一起分享系统的处理器资源。其常用的场景是应用拥有大量的长时间存活的对象数据集(老年代占用大量的堆空间)并且运行在多核处理器的环境中,用户可以使用-XX:+UseConcMarkSweepGC命令行参数显式地指定使用CMS收集器。
但是,建议使用G1收集器替代CMS收集器,因为G1收集器具备更好的特性(在后续文章中将详细论述G1收集器),在JDK新版本中G1作为JVM的默认收集器。
1.1 CMS的性能与结构
类似于其他收集器,CMS收集器也是一个通用GC收集器,因而在minor collection(年轻代的专用GC操作)阶段与major collection(老年代的专用GC操作)阶段都有使用。CMS收集器的目的是在major collection阶段使用独立的GC并发线程去检测可达的存活对象,从而减少GC暂停时间,该过程是与应用并行执行,所以不用停止应用的运行。
为了便于描述CMS收集器的运行机制,将major collection阶段分成四个小阶段,前三个小阶段构成了major collection的前半部分流程,第四个小阶段构成了major collection的后半部分流程,第一小阶段与第三小阶段是GC暂停阶段(stop the world),第二小阶段与第四小阶段是并行操作(not stop the world),第三小阶段的GC暂停时间比第一小阶段的GC暂停时间长。在第一、第三小阶段GC暂停时,GC启动多线程去执行GC操作,在第二、第四小阶段GC启动多线程去检测可达的存活对象以及扫除不可达的非存活对象。由以上分析可知,JVM为了提高GC操作的执行效率,多个阶段是相互交错执行的,当然,minor collection的GC操作也在第一、第三阶段中交错地执行。
1.2 并行操作的异常
由上一节可知,CMS收集器以多线程的方式去检测可达的存活对象或者清除不可达的非存活对象,在这个过程中是与应用并行执行,所以此时存在的可能是,应用也在为新来的存活对象申请堆内存空间,一旦GC收集器完成释放空间之前,新来的存活对象把老年代的堆空间填满了或者待申请的堆内存空间不足,那么这种情况被称之为并发模型的异常情况。异常情况下,JVM的补救措施是,立刻停止应用执行(stop the world)直到GC操作完成。
另外,还有一种异常情况是,应用程序主动调用System.gc()或者外部检测工具中断了GC的 操作,此时JVM会发出中断报告。
1.3 内存不足的异常
如果GC收集器耗时太长(占用98%以上的应用总运行时间),则抛出OutOfMemoryError的异常,详细可参考并行收集器。
1.4 对象疏漏的异常
因为CMS收集器是以并发的模式检测可达的存活对象,所以存在的可能是,在GC收集操作的周期内检测完成之前,那些已经检测的可达的存活对象突然变成不可达的非存活对象,那么GC在该次周期内无法回收这些突变的非存活对象,这种场景存在的原因是检测过程中应用也在并行地执行业务逻辑,因此,只能等到下次GC操作周期再检测并回收这些疏漏的非存活对象。为了满足此场景的性能要求,建议老年代堆空间保留20%以上的空闲空间占比。
1.5 CMS暂停阶段(标记)
根据前面章节的描述,major collection阶段可分为四个小阶段,那么第一与第三小阶段的工作是标记那些比较直接的存活对象,包括那些静态对象、线程堆栈内以及注册器内的引用对象,也包括年轻代堆空间内的引用对象,这些存活对象是直接与应用业务逻辑处理中的调用链强相关的引用。第一小阶段被之为初始化标记阶段,第三小阶段被称之为重新标记阶段,第三小阶段主要是标记那些在第二小阶段遗漏的存活对象(补漏),因为第二小阶段是与应用并行执行,有些存活对象被更新了。
1.6 CMS收集阶段(扫除)
根据前面章节的描述,major collection阶段可分为四个小阶段,第二小阶段是并发检测可达的存活对象,第四小阶段是并发清扫不可达的非存活对象(垃圾对象),因为第二、第四小阶段是与应用并发地执行,所以应用的吞吐率会有所下降。当第四小阶段结束,则该次GC周期结束,此时GC收集器从运行状态转换到等待状态,直到下次GC周期的启动,则GC收集器从等待状态转换到运行状态。
1.7 确定何时启动并发收集
根据前面章节介绍的串行收集器,当老年代的堆空间被填满,则触发major collection操作并停止应用的运行直到GC周期结束。然而,在CMS收集器中,则恰好相反,并发收集的GC周期必须在老年代堆空间被填满之前完成GC的收集工作,否则会出现1.2章节所述的并行操作的异常,这样的异常的出现会让GC暂停时间更长,下面介绍启动并发收集的最佳时机。
JVM会根据最近的运行历史记录执行统计分析,并估算当前时刻到老年代堆空间被填满所剩下的时间,以及估算GC并发操作所需要的时间,根据这些估算的值,JVM会选择一个最佳的时机启动GC并发收集,该机制能保证老年代堆空间被填满之前,结束GC并发收集工作(回收并再利用不可达的垃圾对象的堆空间)。
此外,如果老年代堆空间被占用比例到达临界值,则会自动触发GC并发收集,该临界值可以使用-XX:CMSInitiatingOccupancyFraction=<N>命令行参数配置,JVM提供的默认值约等于92%。
1.8 CMS暂停阶段的调度
由以上分析可知,major collection分为四个小阶段,第一、第三小阶段是GC暂停阶段,另外minor collection也是GC暂停阶段,所以以上三个GC暂停阶段可能会出现连续暂停的情况,这会让GC暂停时间变得更长,从而会让应用的吞吐率下降。因此,JVM会使用平衡的调度机制,重点是让第三个小阶段与minor collection之间不出现连接GC暂停的情况(重新标记阶段比初始化标记阶段的暂停时间大很多)。
1.9 监控与测量
用户可以使用命令行参数-Xlog:gc监控GC的行为日志,举例如下:
[121,834s][info][gc] GC(657) Pause Initial Mark 191M->191M(485M) (121,831s, 121,834s) 3,433ms
[121,835s][info][gc] GC(657) Concurrent Mark (121,835s)
[121,889s][info][gc] GC(657) Concurrent Mark (121,835s, 121,889s) 54,330ms
[121,889s][info][gc] GC(657) Concurrent Preclean (121,889s)
[121,892s][info][gc] GC(657) Concurrent Preclean (121,889s, 121,892s) 2,781ms
[121,892s][info][gc] GC(657) Concurrent Abortable Preclean (121,892s)
[121,949s][info][gc] GC(658) Pause Young (Allocation Failure) 324M->199M(485M) (121,929s, 121,949s) 19,705ms
[122,068s][info][gc] GC(659) Pause Young (Allocation Failure) 333M->200M(485M) (122,043s, 122,068s) 24,892ms
[122,075s][info][gc] GC(657) Concurrent Abortable Preclean (121,892s, 122,075s) 182,989ms
[122,087s][info][gc] GC(657) Pause Remark 209M->209M(485M) (122,076s, 122,087s) 11,373ms
[122,087s][info][gc] GC(657) Concurrent Sweep (122,087s)
[122,193s][info][gc] GC(660) Pause Young (Allocation Failure) 301M->165M(485M) (122,181s, 122,193s) 12,151ms
[122,254s][info][gc] GC(657) Concurrent Sweep (122,087s, 122,254s) 166,758ms
[122,254s][info][gc] GC(657) Concurrent Reset (122,254s)
[122,255s][info][gc] GC(657) Concurrent Reset (122,254s, 122,255s) 0,952ms
[122,297s][info][gc] GC(661) Pause Young (Allocation Failure) 259M->128M(485M) (122,291s, 122,297s) 5,797ms
由以上日志可知,Pause单词出现的行日志表示GC暂停阶段,Concurrent单词出现的行日志表示并发执行阶段,以上的日志体现了5个类别的操作:初始化标记、重新标记、并发查找存活对象、并发清除非存活对象、年轻代的GC操作,下面以表格的形式说明每个操作的意义:
GC操作的名称 | 含义 | 阶段 | 耗时(单位:毫秒) |
Pause Initial Mark | 初始化标记(第一小阶段) | Major collection | 3,433ms |
Concurrent Mark | 并发标记(第二小阶段) | Major collection | |
Concurrent Preclean | 并发预清除(第二小阶段 | Major collection | |
Concurrent Abortable Preclean | 并发预清除(第二小阶段 | Major collection | |
Pause Young (Allocation Failure) | 年轻代的GC收集 | Minor collection | |
Pause Remark | 重新标记(第三小阶段) | Major collection | 11,373ms(明显比初始化标记的GC暂停时间长很多) |
Concurrent Sweep | 并发清除(第四小阶段) | Major collection | |
Concurrent Reset | 为下次并发GC做准备(第四小阶段) | Major collection |
(未完待续)