1.三色标记法缺陷-漏标问题
我们上一篇文章知道了三色标记法存在多标和漏标的问题,多标无伤大雅,漏标才是致命的,对于漏标问题,有什么解决办法呢? 我们知道漏标的两个必要条件
- 条件1-> 灰色对象断开了白色对象的引用
- 条件2-> 黑色对象重新和白色对象建立了引用关系
从代码角度来看,这两个条件其实就是 三步操作
#1.读取A对象的属性 field 的引用值,就是 B,就是灰色的A
var B = objA.field //读
#2.断开A对象的引用B 把A对象的属性field 设置为空,断开与B的链接,B是不可达的垃圾
objA.field = null //写
#3.有个已经扫描过的C对象 重新链接了B,B可达,真实有用对象
objC.field = B //写
那我们是不是破坏这三步中的任何一步,漏标问题的条件就不满足了,问题就不会发生了,比如我们把漏标断开的对象记录下来,记录这些改变的对象,记录完毕后,再次对这些对象重新遍历下,标记下状态,这个过程就叫做重新标记, 重新标记是需要STW的,我们只是重新遍历那些改动的对象,大大的缩短了STW的时间
要破坏这三部中的任何一步,就产生了读写屏障
- 读屏障 拦截第一步,读的过程
- 写屏障 拦截第二步,第三步,写入的过程
- 读写屏障拦截的目的很简单,就是在操作对象断开/链接的过程中, 把操作变更的对象记录下来
2.解决方案
读写屏障拦截的目的很简单,就是在操作对象断开/链接的过程中, 把操作变更的对象记录下来
2.1 读屏障 + 重新标记
读屏障就是直接针对第一步,在读取节点之前进行操作
- 读屏障,在读之前记录下了要读取的对象
- 条件2 黑色对象重新链接,引用了白色的对象
- 在重新引用的前提就是 我们可以获取到白色对象,在获取白色对象的时候,读屏障就发挥作用了
- 我们把这个对象记录下来,等待并发标记结束后STW
- 然后针对所有记录下来的对象,重新标记,重新扫描
- 重新标记的过程一定要STW,否则重新标记完,又有新的变化,无穷无尽
2.2 写屏障 + 增量更新
CMS解决漏标问题就是采用增量更新, 什么是增量更新?
- 增量更新就是当黑色对象 链接 新的 白色对象,重新建立引用关系的时候, 就将这个插入新链接的引用记录下来
- 把这些引用记录全都存下来,等待遍历
- 并发标记扫描结束后, 将这些记录过的引用关系中的黑色对象为根, 重新扫描遍历一次
- 黑色对象一旦新建立了链接,存在新的白色对象的引用之后, 它就变回灰色对象了
- 破化第二个条件,因为黑色对象重新引用了白色对象,并且记录下来重新标记
- 从而保证不会漏标
- 这样避免了漏标问题,但是重新标记阶段必须STW,会导致GC时间变长
CMS破坏的就是第二个条件-> 黑色对象重新和白色对象建立了引用关系
建立链接后重新记录下来,太卡有一个缺点就是因为要对这些关系重新扫描这些黑色的对象,所以会稍微耽误一点时间,但是这点时间相对于 如果你要并发标记整个链路,还是微不足道的,可以接受的,实际的项目中,这瞬间引用关系发生变化的对象还是很少的
2.3 写屏障 + 原始快照(SATB)
G1解决漏标问题的方案就是-原始快照,什么是原始快照?
- 原始快照就是当灰色对象要删除指向白色对象的引用关系时, 就将这个删除链接的引用记录下来
- 这样破坏的是第一个条件,因为灰色对象断开了链接,但是这个删除链接的关系被记录下来
- 尝试保留开始时的对象图,即原始快照(Snapshot At The Beginning,SATB)
- 并发扫描结束之后, 再将这些记录过的引用关系中的灰色对象为根, 重新扫描一次
- 这样后续在扫描的过程中,始终都是按照存在这个被删除的关系,即使D 删除 了G,但是原始的对象快照图中 D依旧和G保持着关系,所以G不会被当作垃圾处理
- 这样就能扫描到白色的对象G,将白色对象直接标记为黑色
- 目的就是让这种被删除的对象,依旧在本轮gc清理中能存活下来
- 当然这个对象也有可能真的是垃圾,浮动垃圾 ,待下一轮gc的时候重新扫描就会被清除
G1 垃圾回收器采用的是原始快照STAM的方案,破坏第一个条件->灰色对象断开了白色对象的引用
我们断开链接前保存快照,这样在快照中,灰色对象和白色对象同样都是由引用关系,后面引用关系发生变化时,快照中引用关系时存在的,这样就不会发生真实对象被当作白色对象回收的问题
但是这种方式也有一个缺点,就是如果这个对象,断开连接后,没有新的对象建立链接,那么他就是垃圾对象,但是快照中又存在引用,导致该垃圾对象不会被回收,就产生了浮动垃圾,相对于致命的缺点存活对象被回收,浮动垃圾相比之下也是可以接受的,因为浮动垃圾伴随着下一次的GC,就会被清楚掉
3.CMS和G1全面对比
CMS增量更新对比G1原始快照,到底哪种方案更好? 我们查阅了一些资料,对比之下G1的解决方案更加优秀,全面对比了两种垃圾收集方法,总结原因如下:
- GC耗时方面
- 因为两种都要对 记录下来的对象进行重新扫描,所以这块两种垃圾处理器差别不大
- STW时间
- CMS号称响应优先处理器,以最小停顿时间为目标
- G1则是以垃圾回收优先,在规定时间内完成性价比最高的回收
- 垃圾碎片
- CMS采用标记清除法,会产生内存碎片
- G1采用标记整理法,对内存空间进行整理,没有内存碎片
- 回收垃圾区域
- CMS回收老年代,搭配新生代ParNew一起使用
- G1回收老年代及新生代,独立使用
- 垃圾回收算法
- CMS 过程分为 1.初始标记->2.并发始标记->3.重新标记->4.并发清除
- G1 过程分为 1.初始标记->2.并发始标记->3.最终标记->4.筛选回收
至此,我们针对三色标记的缺陷漏标问题,给出了完美的解决方案,读屏障+重新标记,或者写屏障+增量更新,或者写屏障+原始快照这几种解决方式,CMS垃圾收集器采用的就是写屏障+增量更新,G1采用的是写屏障+STAB