JVM CMS和G1垃圾回收器
本文将浅谈JVM相关垃圾回收器,并试图分析CMS和G1垃圾回收器的问题与解决。
Serial与Serial Old收集器
顾名思义,串行收集器单线程工作。数据比较老的垃圾收集器。
单线程意味着只有一个线程去回收没有GCROOT指向的对象,并且暂停工作线程(STW),直至收集结束。
回收算法:新生代复制(Serial),老年代标记整理(Serial Old)。
特点:
(1)新生代垃圾收集器。
(2)复制算法
(3)单线程
应用场景:
(1)目前只有hostSpot在Client模式下默认的收集器。
设置参数:
-XX:+UseSerialGC
ParNew收集器
ParNew收集器是Serial收集器的多线程版本,其他与Serial行为一样,回收策略与手机算法等。
并且目前能与CMS收集器搭配合作的只有ParNew,在新生代垃圾回收的时候由于是多个GC线程并发清除所以会很大缩短STW的时间。
回收算法:新生代复制(ParNew),老年代标记整理(Serial Old)。
特点:
(1)多线程回收,其他与serial一样。
(2)老年代依然使用Serial Old
应用场景:
在server端ParNew配合CMS收集器一起工作,但是在单个CPU环境中应该使用Serial收集器,避免线程交互的开销。
设置参数:
指定使用CMS后,会默认使用ParNew作为新生代收集:
"-XX:+UseConcMarkSweepGC"
强制指定使用ParNew:
"-XX:+UseParNewGC"
指定垃圾收集的线程数量,ParNew默认开启的收集线程与CPU的数量相:
"-XX:ParallelGCThreads"
为什么只有ParNew能与CMS收集器配合工作?
(1)CMS是hostSpot在JDK1.5推出的真正意义上的并发垃圾收集器,属于跨时代的垃圾收集器,实现了基本用户线程与GC回收线程同时工作。
(2)CMS作为老年代收集器无法与Parallel Scavenge工作,因为Parallel Scavenge(以及G1)没有使用传统GC收集器的代码框架。
Parallel Scavenge与Parallel Old收集器
Parallel Scavenge是一个新生代收集器,使用复制算法,并且是并行多线程收集器。该收集在新生代老年代都虽然也会STW但会并发进行垃圾回收,关注点是吞吐量。收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量。
回收算法:新生代复制算法(Parallel Scavenge),老年代标记整理(Parallel Old)。
特点:
(1)新生代收集器
(2)采用多线程回收
应用场景:
高吞吐量为目标,即减少垃圾收集时间,让用户代码获得更长的运行时间
设置参数:
控制最大垃圾收集停顿时间"-XX:MaxGCPauseMillis"
设置垃圾收集时间占总时间的比率"-XX:GCTimeRatio"
GCTimeRatio相当于设置吞吐量大小;
垃圾收集执行时间占应用程序执行时间的比例的计算方法是:1 / (1 + n) 。
例如,选项-XX:GCTimeRatio=19,设置了垃圾收集时间占总时间的5% = 1/(1+19);默认值是1% = 1/(1+99),即n=99;
还有一种自动调节的参数来设置,GC自适应的调节策略"-XX:+UseAdptiveSizePolicy"
CMS(Concurrent Mark Sweep)收集器
CMS收集器是一种以获取最短回收停顿时间为目标的收集器。它非常适合在注重用户体验的应用上使用。
回收算法:标记清除(会产生内存碎片)
特点:
(1)针对老年代
(2)基于标记清除算法
(3)并发收集、低停顿
应用场景:
希望系统停顿时间最短,注重服务的响应速度
CMS收集器工作过程:
标记清除:暂停所有的其他线程,虽然暂停其他线程,但是初始标记仅仅标记GC Roots能直接关联到的对象,速度很快;
并发标记:并发标记就是进行GC Roots Tracing的过程,此时用户线程不会停顿,所以叫做并发标记。
重新标记:重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记,可以理解为并发标记期间可能产生了一些新的垃圾对象。此时可能会有STW,但时间也不会长,因为不会设计过多对象。
并发清除:与用户线程一起执行,不会STW,GC清除垃圾对象。
设置参数:
"-XX:+UseConcMarkSweepGC"
缺点:
(1)对CPU资源有要求
对CPU资源比较敏感,占用一部分CPU资源,系统吞吐量可能会稍有降低。
CMS的默认收集线程数量是=(CPU数量+3)/4;当CPU数量越多,回收的线程占用CPU就少。
也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,对用户程序影响可能较大;不足4个时,影响更大,可能无法接受。(比如 CPU=2时,那么就启动一个线程回收,占了50%的CPU资源。)
(2)无法处理浮动垃圾
无法处理浮动垃圾,可能出现"Concurrent Mode Failure"失败
在并发清除时,用户线程新产生的垃圾,称为浮动垃圾;(也就是老年代正在清理,从年轻代晋升了新的对象,或者直接分配大对象年轻代放不下导致直接在老年代生成,这时候老年代也放不下),则会抛出“concurrent mode failure”,
解决办法:可以使用"-XX:CMSInitiatingOccupancyFraction",设置CMS预留老年代内存空间;
(3)产生大量内存碎片
由于CMS是基于“标记+清除”算法来回收老年代对象的,因此长时间运行后会产生大量的空间碎片问题,可能导致新生代对象晋升到老生代失败。
由于碎片过多,将会给大对象的分配带来麻烦。因此会出现这样的情况,老年代还有很多剩余的空间,但是找不到连续的空间来分配当前对象,这样不得不提前触发一次Full GC。
解决办法:开启空间碎片整理,并将空间碎片整理周期设置在合理范围;
-XX:+UseCMSCompactAtFullCollection (空间碎片整理)
-XX:CMSFullGCsBeforeCompaction=n
CMS与Parallbel Old对比
CMS与Parallel Old垃圾收集器相比,cms减少了执行老年代垃圾回收时应用暂停时间,但是由于CMS不进行内存空间整理(节省时间)增加了新生代垃圾收集器应用暂停时间,降低了吞吐量并且占用更大堆空间(不整理虽然快一些但是会有内存碎片)。
G1收集器
随着 JVM 中内存的增大,STW 的时间成为 JVM 急迫解决的问题,但是如果按照传统的分代模型,总跳不出 STW 时间不可预测这点。为了实现 STW 的时间可预测,首先要有一个思想上的改变。G1 将堆内存“化整为零”,将堆内存划分成多个大小相等独立区域(Region),每一个 Region都可以根据需要,扮演新生代的 Eden 空间、Survivor 空间,或者老年代空间。回收器能够对扮演不同角色的 Region 采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。JDK1.9以后G1成为默认垃圾回收器。
回收算法:复制算法
特点:
(1)并行与并发、G1充分利用CPU、多核环境下的硬件优势,使用多核CPU来缩短STW的停顿时间。
(2)分代收集、虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。
(3)将整个堆划分为多个大小相等的独立区域(Region),G1并不要求对象的存储一定是物理上连续的只要逻辑上连续即可,某个分区也不会固定的为某一代服务,可以按需切换,默认规划2048个region,每个region 1MB~32MB。
(4)可预测的停顿、建立可预测的停顿时间模型。可以明确指定M毫秒时间片内,垃圾收集消耗的时间不超过N毫秒。在低停顿的同时实现高吞吐量。
G1如何实现可预测停顿?
G1跟踪各个Region获得其收集价值大小,在后台维护一个优先列表,每次根据允许的收集时间,决定回收的region。
跨代引用问题,一个region中的对象可能会被其他region对象引用,在判断对象存活时是否需要扫描整个堆空间?
在其他垃圾回收器中是会存在这样问题的,回收新生代垃圾对象可能也要扫描老年代,降低Minor GC效率。而G1使用Remembered Set(记忆集)来避免全局扫描,每一个region都有一个RememberedSet,用来记录从其他Region中的对象到本Region的引用,是一种抽象的数据结构。有了这个数据结构,在回收某个Region的时候,就不必对整个堆内存的对象进行扫描了,它使得部分收集成为了可能。
G1相对CMS的优势?
(1)G1使用region区域的方式避免了内存碎片的问题
(2)由于新生代老年代不固定,内存使用效率上来说更加灵活
(3)G1可以设置停顿时间来控制回收时间。
(4)G1在回收后马上做合并空间的工作,而CMS则是在最后STW的时候做。