【面试】你了解哪些收集器?CMS和G1。详细谈谈G1的优点?什么时候进行Full GC呢?

CMS收集器

老年代使用的垃圾收集器
可以搭配老年代SerialOld(MSC)、新生代Serial收集器、ParNew收集器

收集过程

初始标记(CMS initial mark)
这个阶段仅仅只是标记一下GC Roots能直接关联到的对象,速度快
并发标记(CMS concurrent mark)
这个阶段就是从GC Roots的直接关系对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行
重新标记(CMS remark)
这个阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发阶段的时间短。
并发清除(CMS concurrent sweep)
这个阶段,就是清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

三色标记算法

黑色:表示该对象已经被标记过了,且该对象下的属性也全部都被标记过了,例如:GCRoots对象
灰色:对象已经被垃圾收集器扫描过了,但是对象中还存在没有扫描的引用(GC需要从此对象中去寻找垃圾)
白色:表示该对象没有被垃圾收集器访问过,即表示不可达。
当Stop The Word时,对象间的引用是不会发生变化的,因为此时用户线程是中断的,可以轻松完成标记。但是在并发标记的时候,标记期间用户线程还在跑,对象间的引用可能发生变化,多标和漏标的情况就可能会发生。

过程

1.初始时,全部对象都是白色的
2.GC Roots直接引用的对象变为灰色
3.从灰色集合中获取元素;将本对象直接引用的对象标记为灰色;然后将当前的对象标记为黑色。
4.重复步骤3,直到灰色的对象集合全部变为空
5.结束后,仍然被标记为白色的对象就是不可达对象,就视为垃圾对象。

漏标

● 灰色指向白色的引用全部断开(在这就行一个原始快照)
● 黑色指向白色的引用被建立(增量更新)

写屏障

写屏障 + SATB
当对象B的引用发生变化时,利用写屏障,将B原来的引用对象记录下来,这样可以尝试保留开始时的对象图,保证标记依然按照原本的路线走
写屏障 + 增量更新(CMS采用的方法)
●当对象A的引用发生变化时,利用写屏障,将A新的引用对象D记录下来
即当有新的引用插入进来时,记录下新的引用
●这种思路不要求保留原始对象图,而是针对新的引用记录下来等待遍历,即增量更新

读屏障

读屏障针对第一步,当读取引用对象的时候,一律记录下来,显然这种方法非常保守,但是安全。
在Java HotSpot VM中
CMS采用的是:写屏障 + 增量更新
G1采用的是:写屏障 + SATB

缺点
对处理器资源非常敏感。

在并发阶段虽然不会导致用户线程停顿,但却会因为占用一部分线程而导致应用程序变慢,降低总吞吐量。

无法处理“浮动垃圾”。

因为无法处理“浮动垃圾”,就有可能出现“Concurrent Mode Failure”失败而导致另一次完全“Stop The Stop”的Full GC的产生。
在CMS的并发标记和并发清理阶段,用户线程还是继续运行的,程序在运行自然就还有伴随着有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程以后的,CMS无法在当次收集中处理掉它们,就只好期待下一次垃圾收集时再清理掉。这就是“浮动垃圾”。
针对并发标记开始后产生的新对象,通常做法是直接标记为黑色,本轮不进行清除。即使这些对象会变成垃圾对象,这也算浮动垃圾的一部分。

会产生内存碎片

因为CMS是基于“标记 - 清除”算法来实现的收集器。从垃圾回收中就了解到垃圾回收中的“标记 - 清除”算法就会产生内存碎片。内存碎片过多的话,就会导致给大对象分配带来了很大的麻烦。就会不得不提前触发Full GC。

G1收集器

简介

收集器面向局部收集的设计思路和基于Region的内存布局形式。是一款面向服务端应用的垃圾收集器。
在延迟可控的情况下获得尽可能高的吞吐量,所以才会担当起“全功能收集器”的重任与期望。

名词解释

RSet(Remember Set :记忆集合)

划出一部分内存用来储存记录其他Region对当前持有Rset Region中Card的引用,这个记录就叫做Remember Set

G1垃圾回收器,有对STW时间的控制,通过参数 -XX:MaxGCPauseMillis 来设置,而对于整个堆进行一次回收所需要的的实际STW时间可能远远超过这个值,
所以G1可以不用扫描整个堆,只要通过扫描RSet来分析垃圾比例最高的Region区,放入CSet(Collection Set :回收集合)中,进行回收。

Rset的储存方状态,会根据对当前区域中引用数量的增加依次递增,分别为:稀疏(hash)->细粒度->粗粒度。
稀疏状态: 一个其他Region引用当前Region 中Card 的集合 被放在一个数组里面,Key:redion地址 Value:card 地址数组
细粒度: 一个Region地址链表,共同维护当前 Region 中所有card 的一个BitMap集合,该card 被引用了就设置对应bit 为1,并且还维护一个 对应Region对当前Region中card 索引数量
粗粒度: 所有region 形成一个 bitMap,如果有region 对当前 Region 有指针指向,就设置其对应的bit 为1

CSet(Collection Set 回收集合)

CSet of Young Collection
只专注回收 Young Region 跟 Survivor Region
CSet of Mix Collection
CSet of Mix Collection 模式下的CSet 则会通过RSet计算Region中对象的活跃度,活跃度阈值-XX:G1MixedGCLiveThresholdPercent(默认85%),
只有活跃度高于这个阈值的才会准入CSet,混合模式下CSet还可以通过XX:G1OldCSetRegionThresholdPercent(默认10%)设置,CSet跟整个堆的比例的数量上限。

Concurrence Refinement Thread(同步优化线程)

这个线程主要用来处理代间引用之间的关系用的。
当赋值语句发生后,G1通过Writer Barrier技术,跟G1自己的筛选算法,筛选出此次索引赋值是否是跨区(Region)之间的引用。
如果是跨区索引赋值,在线程的内存缓冲区写一条log,一旦日志缓冲区写满,就重新起一块缓冲重新写,而原有的缓冲区则进入全局缓冲区。
Concurrence Refinement Thread 扫描全局缓冲区的日志,根据日志更新各个区(Region)的RSet。
这块逻辑跟后面讲到的SATB技术十分相似,但又不同SATB技术主要更新的是存活对象的位图。
可通过 -XX:G1ConcRefinementThreads (默认等于-XX:ParellelGCThreads)设置。
如果发现全局缓冲区日志积累较多,G1会调用更多的线程来出来缓冲区日志,甚至会调用App Thread 来处理,造成应用任务堵塞,所以必须要尽量避免这样的现象出现。
可以通过阈值
-X:G1ConcRefinementGreenZone/-XX:G1ConcRefinementYellowZone/-XX:G1ConcRefinementRedZone
这三个参数来设置G1调用线程的数量来处理全局缓存的积累的日志。

运行过程

初始标记(Initial Marking)

仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS指针的值,让下一个阶段用户线程并发运行的时候,能正确地在可用的Region中分配新对象。
这个阶段需要停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成时,所以G1收集器在这个阶段实际并没有额外的停顿。

并发标记(Concurrent Marking)

从GC Root开始对堆中的对象进行可达性分析,递归扫描整个堆里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。
当对象图扫描完成之后,还有重新处理SATB记录下的并发时有引用变动的对象。

最终标记(Final Marking)

对用户线程做另一个短暂的暂停,用于处理并发阶段结束后人遗留下的最后那少量的SATE记录。

筛选回收(Live Data Counting and Evacuation)

负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,
然后把决定回收那一部分Region的存活对象复制到空的Region中,再清理掉整个旧的Region的全部空间。
这里的操作涉及存回对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。

FullGC场景

从年轻代分区拷贝存活对象时,无法找到可用的空闲分区
从老年代分区转移存活对象时,无法找到可用的空闲分区
分配巨型对象时在老年代无法找到足够的连续分区

解决FullGC次数过多

(1)降低响应时间或请求次数,这个需要重构,比较麻烦;——这个是终极方法,往往能够顺利的解决问题,因为大部分的问题均是由程序自身造成的。

(2)减少老生代内存的消耗,比较靠谱;——可以通过分析Dump文件(jmap dump),并利用MAT查找内存消耗的原因,从而发现程序中造成老生代内存消耗的原因。

(3)减少每次请求的内存的消耗,貌似比较靠谱;——这个是海市蜃楼,没有太好的办法。

(4)降低GC造成的应用暂停的时间——可以采用CMS GS垃圾回收器。参数设置如下:

-Xms1536m -Xmx1536m -Xmn700m -XX:SurvivorRatio=7 -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection

-XX:CMSMaxAbortablePrecleanTime=1000 -XX:+CMSClassUnloadingEnabled -XX:+UseCMSInitiatingOccupancyOnly -XX:+DisableExplicitGC

(5)减少每次minor gc晋升到old的对象。可选方法:1) 调大新生代。2)调大Survivor。3)调大TenuringThreshold。

调大Survivor:当前采用PS GC,Survivor space会被动态调整。由于调整幅度很小,导致了经常有对象直接转移到了老生代;于是禁止Survivor区的动态调整了,-XX:-UseAdaptiveSizePolicy,并计算Survivor Space需要的大小,于是继续观察,并做微调…。最终将Full GC推迟到2小时1次。

衡量调优

降低Full GC执行频率 – 通常瓶颈

老生代本身占用的内存空间就一直偏高,所以只要稍微放点对象到老生代,就full GC了;
通常原因:系统缓存的东西太多;
例如:使用oracle 10g驱动时preparedstatement cache太大;
查找办法:现执行Dump然后再进行MAT分析;

Minor GC后总是有对象不断的进入老生代,导致老生代不断的满

通常原因:Survivor太小了
系统表现:系统响应太慢、请求量太大、每次请求分配的内存太多、分配的对象太大…
查找办法:分析两次minor GC之间到底哪些地方分配了内存;
利用jstat观察Survivor的消耗状况,-XX:PrintHeapAtGC,输出GC前后的详细信息;
对于系统响应慢可以采用系统优化,不是GC优化的内容;

老生代的内存占用一直偏高

调优方法:
① 扩大老生代的大小(减少新生代的大小或调大heap的 大小);
减少new注意对minor gc的影响并且同时有可能造成full gc还是严重;
调大heap注意full gc的时间的延长,cpu够强悍嘛,os是32 bit的吗?
② 程序优化(去掉一些不必要的缓存)

Minor GC后总是有对象不断的进入老生代

前提:这些进入老生代的对象在full GC时大部分都会被回收
调优方法:
① 降低Minor GC的执行频率;
② 让对象尽量在Minor GC中就被回收掉:增大Eden区、增大survivor、增大TenuringThreshold;注意这些可能会造成minor gc执行频繁;
③ 切换成CMS GC:老生代还没有满就回收掉,从而降低Full GC触发的可能性;
④ 程序优化:提升响应速度、降低每次请求分配的内存

降低单次Full GC的执行时间

通常原因:老生代太大了…
调优方法:
1)是并行GC吗?
2)升级CPU
3)减小Heap或老生代

降低Minor GC执行频率

通常原因:每次请求分配的内存多、请求量大
通常办法:
1)扩大heap、扩大新生代、扩大eden。
注意点:
降低每次请求分配的内存;
横向增加机器的数量分担请求的数量。

降低Minor GC执行时间

通常原因:新生代太大了,响应速度太慢了,导致每次Minor GC时存活的对象多
通常办法:
1)减小点新生代;
2)增加CPU的数量、升级CPU的配置;加快系统的响应速度

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值