G1 垃圾收集器 参考:G1 垃圾收集器入门html
G1 与CMS的区别 参考:CMS收集器和G1收集器优缺点面试
什么是CMS算法
CMS全称 ConcurrentMarkSweep,是一款并发的、使用标记-清除算法的垃圾回收器, 若是老年代使用CMS垃圾回收器,须要添加虚拟机参数-"XX:+UseConcMarkSweepGC"。数据结构
使用场景:并发
GC过程短暂停,适合对时延要求较高的服务,用户线程不容许长时间的停顿。oop
缺点:post
服务长时间运行,形成严重的内存碎片化。 另外,算法实现比较复杂(若是也算缺点的话)url
实现机制线程
根据GC的触发机制分为:周期性Old GC(被动)和主动Old GC,纯属我的理解,实在不知道怎么分才好。日志
周期性Old GC
周期性Old GC,执行的逻辑也叫 BackgroundCollect,对老年代进行回收,在GC日志中比较常见,由后台线程ConcurrentMarkSweepThread循环判断(默认2s)是否须要触发。
触发条件
若是没有设置 UseCMSInitiatingOccupancyOnly,虚拟机会根据收集的数据决定是否触发(线上环境建议带上这个参数,否则会加大问题排查的难度)
老年代使用率达到阈值 CMSInitiatingOccupancyFraction,默认92%
永久代的使用率达到阈值 CMSInitiatingPermOccupancyFraction,默认92%,前提是开启 CMSClassUnloadingEnabled
新生代的晋升担保失败
晋升担保失败
老年代是否有足够的空间来容纳所有的新生代对象或历史平均晋升到老年代的对象,若是不够的话,就提前进行一次老年代的回收,防止下次进行YGC的时候发生晋升失败。
周期性Old GC过程
当条件知足时,采用“标记-清理”算法对老年代进行回收,过程能够说很简单,标记出存活对象,清理掉垃圾对象,可是为了实现整个过程的低延迟,实际算法远远没这么简单,整个过程分为以下几个部分:
对象在标记过程当中,根据标记状况,分红三类:
白色对象,表示自身未被标记;
灰色对象,表示自身被标记,但内部引用未被处理;
黑色对象,表示自身被标记,内部引用都被处理;
假设发生Background Collect时,Java堆的对象分布以下:
一、InitialMarking(初始化标记,整个过程STW)
该阶段单线程执行,主要分分为两步:
标记GC Roots可达的老年代对象;
遍历新生代对象,标记可达的老年代对象;
该过程结束后,对象分布以下:
二、Marking(并发标记)
该阶段GC线程和应用线程并发执行,遍历InitialMarking阶段标记出来的存活对象,而后继续递归标记这些对象可达的对象。
由于该阶段并发执行的,在运行期间可能发生新生代的对象晋升到老年代、或者是直接在老年代分配对象、或者更新老年代对象的引用关系等等,对于这些对象,都是须要进行从新标记的,不然有些对象就会被遗漏,发生漏标的状况。
为了提升从新标记的效率,该阶段会把上述对象所在的Card标识为Dirty,后续只需扫描这些Dirty Card的对象,避免扫描整个老年代。
三、Precleaning(预清理)
经过参数 CMSPrecleaningEnabled选择关闭该阶段,默认启用,主要作两件事情:
处理新生代已经发现的引用,好比在并发阶段,在Eden区中分配了一个A对象,A对象引用了一个老年代对象B(这个B以前没有被标记),在这个阶段就会标记对象B为活跃对象。
在并发标记阶段,若是老年代中有对象内部引用发生变化,会把所在的Card标记为Dirty(其实这里并不是使用CardTable,而是一个相似的数据结构,叫ModUnionTalble),经过扫描这些Table,从新标记那些在并发标记阶段引用被更新的对象(晋升到老年代的对象、本来就在老年代的对象)
四、AbortablePreclean(可中断的预清理)
该阶段发生的前提是,新生代Eden区的内存使用量大于参数CMSScheduleRemarkEdenSizeThreshold默认是2M,若是新生代的对象太少,就没有必要执行该阶段,直接执行从新标记阶段。
为何须要这个阶段,存在的价值是什么?
由于CMS GC的终极目标是下降垃圾回收时的暂停时间,因此在该阶段要尽最大的努力去处理那些在并发阶段被应用线程更新的老年代对象,这样在暂停的从新标记阶段就能够少处理一些,暂停时间也会相应的下降。
在该阶段,主要循环的作两件事:
处理 From 和 To 区的对象,标记可达的老年代对象
和上一个阶段同样,扫描处理Dirty Card中的对象
固然了,这个逻辑不会一直循环下去,打断这个循环的条件有三个:
能够设置最多循环的次数 CMSMaxAbortablePrecleanLoops,默认是0,表示没有循环次数的限制。
若是执行这个逻辑的时间达到了阈值 CMSMaxAbortablePrecleanTime,默认是5s,会退出循环。
若是新生代Eden区的内存使用率达到了阈值 CMSScheduleRemarkEdenPenetration,默认50%,会退出循环。(这个条件可以成立的前提是,在进行Precleaning时,Eden区的使用率小于十分之一)
若是在循环退出以前,发生了一次YGC,对于后面的Remark阶段来讲,大大减轻了扫描年轻代的负担,可是发生YGC并不是人为控制,因此只能祈祷这5s内能够来一次YGC。
...
1678.150:[CMS-concurrent-preclean-start]
1678.186:[CMS-concurrent-preclean:0.044/0.055secs]
1678.186:[CMS-concurrent-abortable-preclean-start]
1678.365:[GC 1678.465:[ParNew:2080530K->1464K(2044544K),0.0127340secs]
1389293K->306572K(2093120K),
0.0167509secs]
1680.093:[CMS-concurrent-abortable-preclean:1.052/1.907secs]
....
在上面GC日志中,1678.186启动了AbortablePreclean阶段,在随后不到2s就发生了一次YGC。
五、FinalMarking(并发从新标记,STW过程)
该阶段并发执行,在以前的并行阶段(GC线程和应用线程同时执行,比如你妈在打扫房间,你还在扔纸屑),可能产生新的引用关系以下:
老年代的新对象被GC Roots引用
老年代的未标记对象被新生代对象引用
老年代已标记的对象增长新引用指向老年代其它对象
新生代对象指向老年代引用被删除
也许还有其它状况..
上述对象中可能有一些已经在Precleaning阶段和AbortablePreclean阶段被处理过,但总存在没来得及处理的,因此还有进行以下的处理:
遍历新生代对象,从新标记
根据GC Roots,从新标记
遍历老年代的Dirty Card,从新标记,这里的Dirty Card大部分已经在clean阶段处理过
在第一步骤中,须要遍历新生代的所有对象,若是新生代的使用率很高,须要遍历处理的对象也不少,这对于这个阶段的总耗时来讲,是个灾难(由于可能大量的对象是暂时存活的,并且这些对象也可能引用大量的老年代对象,形成不少应该回收的老年代对象而没有被回收,遍历递归的次数也增长很多),若是在AbortablePreclean阶段中可以刚好的发生一次YGC,这样就能够避免扫描无效的对象。
若是在AbortablePreclean阶段没来得及执行一次YGC,怎么办?
CMS算法中提供了一个参数: CMSScavengeBeforeRemark,默认并无开启,若是开启该参数,在执行该阶段以前,会强制触发一次YGC,能够减小新生代对象的遍历时间,回收的也更完全一点。
不过,这种参数有利有弊,利是下降了Remark阶段的停顿时间,弊的是在新生代对象不多的状况下也多了一次YGC,最可怜的是在AbortablePreclean阶段已经发生了一次YGC,而后在该阶段又傻傻的触发一次。
因此利弊须要把握。
主动Old GC
这个主动Old GC的过程,触发条件比较苛刻:
YGC过程发生Promotion Failed,进而对老年代进行回收
System.gc(),前提是添加了-XX:+ExplicitGCInvokesConcurrent参数
若是触发了主动Old GC,这时周期性Old GC正在执行,那么会夺过周期性Old GC的执行权(同一个时刻只能有一种在Old GC在运行),并记录 concurrent mode failure 或者 concurrent mode interrupted。
主动GC开始时,须要判断本次GC是否要对老年代的空间进行Compact(由于长时间的周期性GC会形成大量的碎片空间),判断逻辑实现以下:
*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 */));
在三种状况下会进行压缩:
其中参数 UseCMSCompactAtFullCollection(默认true)和CMSFullGCsBeforeCompaction(默认0),因此默认每次的主动GC都会对老年代的内存空间进行压缩,就是把对象移动到内存的最左边。
固然了,好比执行了 System.gc(),也会进行压缩。
若是新生代的晋升担保会失败。
带压缩动做的算法,称为MSC,标记-清理-压缩,采用单线程,全暂停的方式进行垃圾收集,暂停时间很长很长...
那不带压缩动做的算法是什么样的呢?
不带压缩动做的执行逻辑叫 ForegroundCollect,整个过程相对周期性Old GC来讲,少了Precleaning和AbortablePreclean两个阶段,其它过程都差很少。
CMS的算法很复杂,一篇文章中不可能面面俱到,若是文章中说的有问题、或者有疑问能够留言讨论。