Java基础之《JVM性能调优(9)—CMS垃圾回收器》

一、为什么会出现CMS垃圾回收器

1、由于历代垃圾回收器都是串行或独占式回收的,例如前面3个年轻代回收器(Serial、ParNew、Parallel) + 2个老年代回收器(Serial Old、Parallel Old),都是必须停止工作线程后,gc线程才开始垃圾清除。

2、在这样的大背景下,与2002年JDK1.4.2发布CMS,它是那个时代第一次实现并发收集器(相对来说),即实现了让垃圾收集线程与用户线程同时工作。

3、CMS的特色就是停顿时间短(低延迟),停顿时间越短就越适合用户交互的程序,越能提升用户体验。

4、在G1收集器面式之前,CMS基本都是JVM的标配,甚至是现在市面都是很多系统在使用CMS。

二、CMS如何让垃圾回收线程和用户线程同时工作
它设计了7个阶段,来保证并行工作。

三、CMS垃圾回收器原理1—初始标记

1、第一阶段:初始标记

(1)先把所有工作线程挂起,所有工作线程进入安全点SafePoint后,当前处于STW状态。
(2)gc线程,采用多线程,从GCRoot 标记直接可达的对象(XYZ)。CMS是老年代回收器,所以只标记老年代的。
什么是直接可达?
从栈帧、方法区(静态变量、常量)、本地方法栈等,查找第一个引用对象。
例如:直接引用的对象都是(acdYZ),就被标记为直接可达。其他对象不标记。

四、CMS垃圾回收器原理2—并发标记

1、第二阶段:并发标记

2、gc线程本阶段干什么?
通过遍历第一个阶段(初始标记)标记出来的存活对象,继续递归遍历老年代,并标记可直接或间接到达的所有老年代存活对象(J)。

3、工作线程本阶段干什么?
(1)工作线程的栈帧,继续生产新的对象,继续往eden区填对象。
在年轻代生产
(2)工作线程继续运行,旧的栈帧就出栈,出栈后老对象就死亡,即存活对象转变为垃圾对象。

由于该阶段工作线程还一直在生产新对象和老对象变成垃圾对象。
那这阶段新产生的对象和垃圾对象,如何处理?
(1)不处理?不处理的话就会遗漏,导致垃圾回收不干净。
(2)处理?处理的话,就非常复杂,要对整个老年代再重新GCRoots,重新标记,即费时又费力。

4、那有什么好的算法能快速解决?
JVM设计了一个cardtable来记录并发阶段老年代对象变更后的存储。

具体技术实现如下:
(1)先把内存划分为大小相同的Card(卡片),每个Card的大小为512Byte,每个Card可以装1个或多个对象。

(2)整个老年代的card连起来就成了一张cardtable,记录可能存在的老年代中有新生代的引用的对象地址,来避免扫描整个老年代。
例如,工作线程在此阶段:
2-1)老年代对象的引用关系变更:Y的栈帧出栈,Y就没人引用了,故把Y的card对应的cardtable的value设置为1(dirty)
2-2)直接在老年代分配对象:surivor空间不足,直接分配K、L、M对象,对应的card也设置为dirty
2-3)新生代的对象晋升到老年代:D从新生代晋升到老年代,对应card也设置为dirty

并发标记阶段只负责将引用发生改变的card标记为dirty状态,不负责处理。

五、CMS垃圾回收器原理3—预清理

1、第三阶段—预清理
此阶段工作线程也是不停的,它和gc线程同时工作。
在并发预清理阶段,将会重新扫描《并发标记阶段》被标记为dirty的card,并标记直接或间接引用的对象,然后清除card标识。

gc线程,扫描cardtable,把标记为dirty的card找出来,把card里面的对象,重新GCRoots标记可直接或间接到达的对象。
例如经过此轮的整理,老年代存活对象新增D、M、K。


六、CMS垃圾回收器原理4—可中断的预清理

1、第四阶段—可中断的预清理
此阶段工作线程也是不停的,它和gc线程同时工作。
此阶段触发的前提是,eden区的内存使用量大于参数CMSScheduleRemarkEdenSizeThreshold默认是2M。
如果eden区对象太少,就没必要执行,直接跳下一阶段《重新标记》。

2、为什么会设计这个阶段,它有什么作用?
因为下一阶段《重新标记阶段》会出现STW,为了使暂停时间最短,CMS设计了《预清理阶段》和《可中断的预清理》,目的就为了给《重新标记阶段》降低压力,保障STW暂停时间最短。

3、在该阶段,主要循环做两件事情(可中断是指可以中断这个循环)
(1)处理survivor区的对象,标记可达的老年代对象。
(2)和上一个阶段《预清理阶段》一样,处理cardtable的对象。

4、该阶段还为了控制执行时间,设计了3个中断的条件
(1)设置最多循环的次数CMSMaxAbortablePrecleanLoops,默认0,表示没有循环次数的限制。
(2)如果这个阶段的时间到达了阈值CMSMaxAbortablePrecleanTime,默认是5s,会退出循环。
(3)如果eden区的内存使用率达到了阈值CMSScheduleRemarkEdenPenetration,默认50%,会退出循环。

5、总之一句话:设计该阶段的目的就是为了给下一阶段《重新标记阶段》降低压力,保障STW暂停时间最短。

七、CMS垃圾回收器原理5—重新标记

1、第五阶段—重新标记
重新标记也称为最终的标记。《预清理阶段》和《可中断的预清理》都是为重新标记阶段做准备。
由于重新标记阶段会发生(STW),所以要保证尽可能的停顿时间短,不然就会影响应用程序的用户体验。

2、干3件事
第一件事:扫描整个年轻代

为什么要扫描新生代?
因为对于老年代中的对象,如果被新生代中的对象引用,那么就会被视为存活对象。即使新生代对象已经不可达了,也会使用这些不可达的对象当做CMS的GCRoot来扫描老年代。
例如A的栈帧出栈或A断开B的引用,导致X一直被误认为是GCRoot,像这种为了判断X死亡,只能扫描新生代了。
判断X,要先判断B是不是死亡了,判断B,要先判断A是不是死亡了,判断A,要先判断栈帧是不是断开,所以要扫描整个年轻代。

扫描整个新生代,耗时会比较长,如何优化?
当大量引用老年代的新生代对象死亡时,耗时较长的时候,可加入参数-XX:+CMSScavengeBeforeRemark,在重新标记之前,先执行一次ygc,回收掉年轻代的对象无用的对象,并将对象放入幸存带或晋升老年代,这样再进行年轻代扫描时,只需要扫描幸存区的对象即可,一般幸存带非常小,这大大减少了扫描时间。

第二件事:扫描GCRoots
扫描的目的是,例如J是在《并发标记》扫描出来的,如果后面Z断开了J的引用关系,是不是还得继续扫描一遍。

第三件事:扫描cardtable
和《预清理阶段》一样,gc线程,扫描cardtable,把标记为dirty的card找出来,把card里面的对象,重新GCRoots标记可直接或间接到达的对象。

所以可以看出,在整个过程中,该过程《重新标记》是最慢的,做的事情太多。

八、CMS垃圾回收器原理6—并发清理

1、第六阶段—并发清理
并发清理阶段,主要工作是清理所有未被标记的死亡对象,回收被占用的空间,例如清理Y、L、J对象。

九、CMS垃圾回收器原理7—并发重置

1、第七阶段—并发重置
该阶段重置CMS收集器的数据结构,等待下一次垃圾回收。
 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值