CMS垃圾回收器和G1垃圾回收器
1.CMS垃圾回收器
1.1-标题CMS的含义
CMS:Concurrent Mark Sweep,(自译:并发标记清除)
CMS垃圾回收器是一种以尽可能短的STW时间为目标的收集器,与Parallel Scavenge这种专注于高系统吞吐量的垃圾回收器相反,CMS垃圾回收器降低了系统吞吐量,频繁而短暂的垃圾回收。
我的理解:对于垃圾回收有两种方案,注重系统吞吐量和注重最短STW时间,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。
所以这两种方案在实际看来应该是互斥的:即高吞吐量意味着停顿时间更长,而追求更短的停顿时间必然导致更低的系统吞吐量(因为垃圾收集次数会增多)
1.2-CMS执行垃圾回收的过程
CMS垃圾回收主要分为以下四步:
- 初始标记
会导致STW,暂停其他线程,使用一条垃圾回收线程标记存活的GC Roots对象。
由于只扫描Roots对象,这个过程很快就能结束 - 并发标记
恢复其它线程的运行,同时垃圾回收线程与用户线程并行,根据初次标记的GC Roots对象依照根可达算法,扫描出全部的幸存对象 - 重新标记
会导致STW,重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始阶段稍长一些,但远比并发标记的时间短。 - 并发清除
根据标记的结果,进行垃圾对象的回收,而这个过程也是并发执行的,所以在并发清理阶段进行的改动,如存活对象断开了引用变成了垃圾等等情况,这些垃圾我们称之“浮动垃圾”,会留待下次垃圾回收进行清理。
2.G1垃圾回收器
G1垃圾回收器没有依照CMS垃圾回收器那种分代模型,将堆划分成一个个Region区域(默认为2048个),而每个Region既可能是新生代,也可能是老年代,从整体上来看G1采用的是标记整理算法,而从两个Region的关系来看则是采用复制算法
2.1-G1垃圾回收的过程
G1垃圾回收器的垃圾回收过程与CMS在总体上相同,都需要经过初始标记,并发标记,最终标记和筛选回收的过程。
3.G1和CMS的区别
3.1-CMS的优缺点
- 优点
- 第一个实现并发收集处理的垃圾回收线程。响应速度比以往的垃圾回收器有了质的飞跃
- 缺点
- 采取的是标记清除算法,会导致内存碎片化,不利于内存空间的利用率
- CMS无法处理浮动垃圾,事实上为了避免浮动垃圾导致堆内存溢出,CMS在堆空间达到80%-90%时就会进行Full GC,然后这样还是有OOM的风险,当并发清理阶段的浮动垃圾在瞬间增多超过了当前剩余内存的最大值时,CMS会强制赞停所有线程,使用一条垃圾回收线程扫描整个堆空间进行垃圾回收而这个停顿时间可能非常长
3.2-G1的优缺点
- 优点
- 可以有计划地避免在整个Java堆中进行全区域的垃圾收集
- 采用标记整理算法,不会产生碎片化内存
- 缺点
- 相较于CMS,G1还不具备全方位、压倒性优势。比如在用户程序运行过程中,G1无论是为了垃圾收集产生的内存占用(Footprint)还是程序运行时的额外执行负载(Overload)都要比CMS要高。
4.JVM实现并发收集的问题和解决
4.1-分代收集带来的问题
我们知道,JVM在进行垃圾收集时,需要先标记所有可达对象,然后再清除不可达对象
解决:垃圾回收器根据GC Roots作为寻找可达对象的起点,通过这些起始引用,可以快速的遍历出存活对象。
同时暴露的问题:如果老年代对象引用了新生代的对象,则需要在去老年代进行扫描,标记存活对象。由于年轻代的垃圾回收通常十分频繁,这样频繁的跨代扫描带来不可忽略的性能消耗
最终解决:基于卡表(Card Table)的设计,通常将堆空间划分为一系列2次幂大小的卡页(Card Page)。卡表(Card Table),用于标记卡页的状态,每个卡表项对应一个卡页。
当对一个对象引用进行写操作时(对象引用改变),写屏障逻辑将会标记对象所在的卡页为dirty
G1基于Rset:对于跨代引用,G1采用了Rset记忆集,每个Region都有一个自己的Rset用来记录有哪些Region有指向本Region的引用
其实RSet就是卡表的实现,卡表是数组,而RSet是HashTable。
举例:在垃圾回收时—>新生代Region的Rset记录了老年代Region对本Region内对象的引用。这样,在扫描年轻代Region发现有来自老年代Region的引用时就可以快速的定位要扫描的Region而不用进行范围查找。
卡表:每个region由若干个Card(512byte,card是堆内存最小可用粒度)构成,一个对象通常会占用一个region的若干个card,而每个card都会记录在每个Region独立的CardTable即卡表中。GC就是对region的card进行处理。
RSet:一种points-in结构,在卡表基础上实现,每个Region一个,用来记录别的Region指向自己的指针,并标记这些指针在卡表哪些范围内。
CSet:值得一提的是,除了Rset,G1还维护了一个全局的CSet用来区分需要释放的Region和需要存活的Region
三色标记法
给垃圾回收扫描到的对象“上色”
1.对于垃圾对象或者还没有扫描到的对象:白色
2.对于已经扫描到的对象,但是他引用的对象还没有全部扫描完:灰色
3.对于已经扫描到的并且其所引用的对象也已经完成扫描的:黑色
垃圾回收的过程就是确定黑色的存活对象,并且将白色的垃圾对象清除。
但是三色标记法也可能出问题: 解决不了漏标问题
设想一种情况,A引用了B,B引用了C。此时垃圾回收线程完成了对A的扫描并且扫描到了B。所以A被确定为黑色之后不会在根据A往下扫描存活对象了。而B被标识为灰色,因为它下面还有一个D对象还没被扫描到,此时由于垃圾回收线程和用户线程时并行的,有可能此时的用户线程修改了B的引用即删除B到D的引用,同时新增一条A到D的引用。从结果上来看这个D对象应该被存活,而不幸的是,对于垃圾回收线程来说,因为A对象已经是黑色的了,表示其所有的引用已经扫描完毕,所以再次标记时并不能够由A扫描到D这个对象。因此结果就是:D被漏标,D对象被清除。
为了解决这个问题,G1和CMS采取了不同的策略
G1:写内存屏障+原始快照
原始快照的做法:所谓原始快照,这个快照说相对修改对象引用操作来的,G1在初次标记时会生成对象的引用快照,写屏障在B对象删除D对象的引用时会将B原来的引用对象D记录下来即当原来成员变量的引用发生变化之前,记录下原来的引用对象。这个操作能保证这个引用对象在本轮垃圾回收中存活(将D标识为黑色)
CMS:写内存屏障+增量更新
增量更新的做法:对于并发阶段新增的引用,这里指A到D的引用,由内存屏障记录,将A新的引用成员变量记录下来。在重新标记阶段会将A变为灰色进行重新标记
,这样也能保证D对象的存活