理解Hotspot JVM CMS垃圾回收器【译】

最近在看Cassandra的内存管理方式,Cassandra默认采用的老年代GC回收策略是CMS,以及采用了Slab Allocator的内存分配方式。采用了Slab Allocator的内存分配方式,大大缩短了GC的时间,据说有1000倍。很吸引人。所以,我要深入研究一下这部分。这部分的实际驱动并不大,可能研究并不十分深入。下面有关CMS GC的部分大多是翻译自Understanding GC pauses in JVM, Hotspot's CMS collector. 此外,还有我之前的一些理解。在以后的博客中,我会对JVM平台的延迟问题进行深入的研究。而且,我相信,这并不是一个陈腐的决定。 【正文开始】 Concurrent Mark Sweep(并发标记清除:CMS)是一种Hotspot JVM上的暂停时间比较短的垃圾回收机制。在整个CMS的垃圾回收过程中,大部分是可以和用户的应用并发执行的,不需要暂停用户的应用。但是为了,保证正确的运行,CMS仍旧需要少量的stop-the-world暂停。这篇博客就是来解释这些暂停的特点,以及如何最小化这些暂停的时间。 CMS的基本特点CMS是一个分代的回收器,意味着堆被划分为年轻代和老年代,并且是独立进行垃圾回收的。至于年轻代的回收,JVM也提供了集中方法,可以查阅文档。而CMS是只针对老年代的垃圾回收器。打开CMS垃圾回收器,只需要在Java程序启动时,加上命令行参数:-XX:+UseConcMarkSweepGC 。 CMS收集周期,有以下几个阶段:

  • 初始标记(init mark):收集根引用,这是一个stop-the-world阶段。
  • 并发标记(concurrent mark):这个阶段可以和用户应用并发进行。遍历老年代的对象图,标记出活着的对象。
  • 并发预清理(concurrent preclean):这同样是一个并发的阶段。主要的用途也是用来标记,用来标记那些在前面标记之后,发生变化的引用。主要是为了缩短remark阶段的stop-the-world的时间。
  • 重新标记(remark):这是一个stop-the-world的操作。暂停各个应用,统计那些在发生变化的标记。
  • 并发清理(concurrent sweep):并发扫描整个老年代,回收一些在对象图中不可达对象所占用的空间。
  • 并发重置(concurrent reset):重置某些数据结果,以备下一个回收周期开始。
与大多数其他的垃圾回收器不同,CMS并不进行堆的Compaction操作。也就是说,并不会将清理出来的空间进行移动,使得空闲空间连续。而只是保留内存的碎片。这样做主要是避免了移动对象的巨大开销(而且,这个过程是需要stop-the-world的)。但是这样是会有隐患,产生一次full-gc的。(本文开始提到的Slab Allocator的采用,主要就是针对,CMS的内存碎片较多的缺点)。
CMS的暂停时长
初始标记
在初始标记期间,为了能够开始标记老年代,CMS应当收集所有的根引用。包括:
  • 来自线程栈的引用
  • 来自年轻代的引用
来自栈的引用收集得非常快,通常小于1ms,但是对于来自年轻代的引用收集的速度就依赖于年轻代对象的多少了。正常的,初始标记一般在年轻代完成回收的时候开始,这个时候,年轻代中的eden区是空的,只有活着的对象在from或者to的survivor区。survivor区通常很小,那在年轻代完成回收之后的初始标记,通常是非常快的,低于毫秒级的。但是,如果初始标记的开始的时候,eden区是满的,那就需要稍微长一些的时间。
一旦触发了CMS回收操作,JVM可能会等待一些时间,让年轻代先进行回收,然后开始初始标记。通过设置命令行参数:-XX:CMSWaitDuration=可以设置等待时间。如果要尽可能缩短初始标记的时间,那么,就需要统计年轻代回收时间,将前面的参数设定为一个合理的值。
重新标记
在CMS中,大多数的标记都是并发进行的,但这是不准确的,因为在标记的过程中,引用是会发生变化的。当并发标记结束之后,需要stop-the-world,确保对象图中可达的对象,都是标记为alive。但是回收器并不需要遍历全部的对象图,只需要遍历从标记开始到当前发生变化的引用即可,同时,线程栈和年轻代需要重新扫描一遍。
通常情况下,重新标记的大多数时间,都消耗在扫描年轻代。根据前面的经验,如果,在重新标记之前,年轻代完成了垃圾回收,这个时间将会大大缩短。JVM就恰恰提供了这样的功能,在每次重新标注之前,强制年轻代的垃圾回收。使用参数-XX:CMSScavengeBeforRemark 可以打开这个设置。
即使年轻带是空的,重新标记阶段还是要扫描老年代中修改的引用,通常,这里所消耗的时间和年轻代的回收时间相当。
CMS什么时候启动
与其他老年代的垃圾回收器相比,CMS在老年代空间占满之前就应该开始。CMS收集会在老年代的空闲时间少于某一个阈值的时候被触发(这个阈值可以是动态统计出来的,也可以是固定设置的),而实际的回收周期可能要延迟到下一次年轻代的回收。为什么要这样,前面已经有解释了。
在某些极端恶劣的情况下,对象会直接在老年代中进行分配,并且CMS回收周期开始的时候,eden区尚有非常多的对象。这个时候初始标记阶段会有多于10-100倍的时间消耗。这个通常是因为要分配非常大的对象。几兆的数组等。为了尽量避免长时间的暂停,我们需要合理的配置-XX:CMSWaitDuration。
配置固定的CMS启动阈值:
  • -XX:+UseCMSInitiatingOccupancyOnly
  • -XX:MCSInitiatingOccupancyFraction=70
显示调用MCS周期
  • -XX:+ExlicitGCInvokesConcurrent
CMS的全量GC
 如果CMS不能够在老年代清理出足够的空间,会导致异常,使得JVM临时启动Serial Old垃圾回收方式进行回收。这个会造成长时间的stop-the-world暂停。全量的GC的原因可能有两个:
  • CMS垃圾回收的速度跟不上了
  • 老年代中有大量的内存碎片
当然,也有可能是,没有为JVM分配足够多的内存,从而导致OutofMemoryException。
永久代的回收
一个导致CMS需要进行全量GC的原因是永久代中的垃圾。默认情况下,CMS是不回收永久代中的垃圾的。如果在你的应用中使用了多个类加载器,或者反射机制,那么就需要对永久代进行回收。采用参数-XX:+CMSClassUnloadingEnabled会打开永久代的垃圾回收。
利用多核
通过使用以下的选项,可以使得CMS充分利用多核:
  • -XX:+CMSConcurrentMTEnabled  在并发阶段,可以利用多核
  • -XX:+ConcGCThreads 指定线程数量
  • -XX:+ParallelGCThreads 指定在stop-the-world过程中,垃圾回收的线程数,默认是cpu的个数
  • -XX:+UseParNewGC 年轻代采用并行的垃圾回收器
【完】
Cassandra采用的Slab Allocator就是为了克服CMS在老年代有大量的碎片。官方以及一些其他的文档,都说效果非常好。后面的博客,会结合代码,分析一下实现。

posted on 2012-09-14 18:17  sing1ee  阅读(2888)  评论(1编辑  收藏  举报