jvm三色标记法


前言

我们都知道jvm内存回收都是采用可达性算法实现的,在开始之前,需要先通过选择一些固定的对象作为gc roots,从该对象开始沿着引用链向下遍历,如果存在任意对象到gc root不可达,则表示该对象在内存中处于被“孤立”状态,此时它就是gc进行内存回收的对象。前边也谈了如何找到这些固定的gc roots对象,在选取gc roots的时候,为了保证内存的一致性,不得不在这个时刻暂停用户线程,于是乎,为了解决这个问题,就对gc roots枚举过程进行了一揽子的优化措施,譬如OopMap,安全点,内存屏障等,经过这一系列优化之后,尽管暂停用户线程这件事依然是个绕不过去的坎,但是停顿时间已经变得非常小,加之能作为gc roots的对象相对整个堆空间少之又少,所以整体来看,这个线程停顿时间是可以接受的。


1.可达性分析

当gc roots搜索完成后,接下来才是真正的对内存中的对象进行标记清除的操作,使用的算法自然就是可达性分析算法,在这过程中,与gc roots枚举是一样的,对象并不是孤立存在,对象之间存在引用关系,而且引用关系也在随时发生变化,一个应用运行过程中,任意时刻的内存状态、对象间的引用关系千变万化。那么这个时候,进行可达性分析的时候也必须要暂停用户线程,与gc root枚举不一样的是,这些“平凡的对象”占据了内存的极大部分,对象繁多,而且随着内存增加,对象数量也跟着增多,如果这个时候停掉用户线程,去遍历内存中的对象引用链,找到我们想要pass掉的那些垃圾,势必会对上层应用运行产生负面的影响,尤其是一些对延时要求较高的应用。为了优化这个问题,必须让用户线程和gc并发工作,才有可能减少客户端的停顿时间,提高时效性,这个时候引进了“三色标记法”来解决用户线程和gc的并发执行。

2.三色标记法

根据对象是否被访问过这个条件,将对象标记为三种颜色

  • 白色:表示对象没有被垃圾回收器访问过,如果在可达性分析结束后,依旧存在白色对象,则表示该对象不可达
  • 灰色:表示对象已经被垃圾回收器访问过,但是该对象上至少还有一个引用没有被扫描过
  • 黑色:表示对象已经被垃圾回收器访问过,而且这个对象所有引用都已经被扫描过,此时,他是存活的,如果有其他对象引用指向了黑色对象,无需重复扫描一遍,黑色对象不可能不经过灰色对象直接指向白色对象

以下图为例:
在这里插入图片描述
初始状态,只有gc roots被标记为黑色,其他对象均是白色,箭头指向代表了对象引用方向。

在这里插入图片描述
当从gc root开始向下遍历,如图所示,只要是引用可达的对象会是一个逐渐黑化的过程,白>灰>黑。
而没有被引用的对象,则表示对象不可达,比如上图中的f &g节点,即使g节点依旧被f引用,但它依旧是不可达,依旧是被清理的目标,要怪只能怪它爹不靠谱,不是别人的依赖者,条条大路通罗马,他却脱离了大路…
在这里插入图片描述
当遍历完整个引用链后,最终存活的对象都会被标记为黑色,白色的对象都会被视作垃圾,且最终的结果中不可能会存在灰色对象,要么黑,要么白,非生即死,不存在植物人对象。

以上流程是比较容易理解,但是忽略了一个问题,这种情况如果是用户线程暂停的情况下,只有gc线程在工作,是没有问题的。上边提到,为了降低客户端的延时,整个标记清理过程必须是gc和用户线程并发,如果是在并发情况下,上述情况就会出现问题,由于对象引用关系一直在变化,gc同时也在工作,这个时候就会导致两个问题:

  1. 将原来死亡的对象标记为存活,(在扫描过程中,趁gc扫描过去,老同志不讲武德突然变卦,引用失效)
  2. 将原来存活的对象标记为死亡,(老同志又不讲武德,梅开二度,很快啊,又突然重新建立了引用)

从以上两种情况可以看出,第一问题看上去还凑合,问题不大,只是这个对象引用的突然变卦导致垃圾没有被扫描到,称为浮动垃圾,在下次gc被回收就可以了。
但是第二种情况就很严重了,将本来就存活的对象,标记为死亡,这会直接导致应用程序报错。
如图所示:
在这里插入图片描述
以上这种情况只是应该被清理的对象没有被标记到,对于gc而言,下次回收即可。
再看下一种情况,这种情况问题就比较严重了
在这里插入图片描述
可以看到问题出现了,本来不应该被当做垃圾的对象被当做垃圾标记清理了,这会导致上层应用直接翻车。

通过以上观察可以发现,对于一个对象,必须同时满足两个条件才会导致不该被清理的对象被清理。

  1. 至少有一个黑色对象在被gc标记后重新指向了白色对象
  2. 灰色对象在自己扫描完成之前断开了对白色对象的直接引用或者间接引用

所以只要破坏以上其中任意一个条件就可以
目前有两种方式:

  1. 增量更新
  2. 原始快照

增量更新破坏的是第一个条件,即当黑色对象在扫描完成后重新建立了对白色对象的引用,发生这种情况后,就将这个黑色对象记录下,待并发扫描结束后,再以这个黑色对象为根,重新扫描一次。
怎么理解?对于新增的这个对象引用,它不能死,不能不管他,否则会被视为垃圾,那我就把这个引用关系记下来,从黑色节点重新扫描一遍,而至于其他对象到这个新增的对象引用是否删除,无所谓,反正我必须让它活,活下来就好了啊。
原始快照破坏的是第二个条件,即灰色对象在自己扫描完成之前断开了对白色对象的引用,当发生了这种情况后,就将灰色对象引用的白色对象记录下,待并发扫描结束后,再从白色对象开始重新扫描标记一次。这种方式有一个缺点就是前边提到的浮动垃圾,需要“期待”下次gc将其回收。
怎么理解?灰色节点扫描过程中,有一个引用断开了,灰色对象不像黑色对象,扫描完标记为黑色后,就不会回头,灰色引用无论是新增的引用还是删除的引用,它是能感知到的,所以删除了的引用如果不去管它,把它视作垃圾,万一删除这个引用所指向的对象又突然被黑色节点所引用,黑色对象是已经确定的对象啊,不会回头啊(那我让它回头,黑色节点新增了引用,我给他记下来,那不就可以了,对,这不是就增量更新?这里就能够看出,两个算法选其一实现即可),这不炸了嘛?所以我删除前要将这个引用给记下来,并且并发标记结束后从这个灰色对象开始,把他们重新扫描一遍,不管它们有没有被黑色对象重新引用,我都让它们活下来,这也就说明了为什么会产生浮动垃圾,因为删除之后可能是真的变成垃圾了,没有被黑色重新引用,但是为了以防万一,我必须让它活着。

再从字面意思理解,增量更新和原始快照的取名意义:
增量更新:面向黑色新增的引用,黑色已经扫描过不会回头,必须有一种机制来感知这个新增引用,然后做一次二次扫描,对新增的对象引用进行更新。
原始快照:面向灰色删除的引用,灰色无论新增还是删除,都会感知到,新增无所谓,正常扫描就行了,删除可不行,删除之前什么样,删除之后也得是什么样,鬼知道它会不会被黑色对象重新引用,所以这就叫原始快照

到这里还有一丝疑问,原始快照删除之前什么样,删除之后也得是什么样,那为什么还要扫描一遍?
答:删除之前的引用指向的对象,包括它一大堆的间接引用,有没有可能又引用了之前被当作垃圾的对象(白色对象),这个引用被黑色重新引用后,还是会出问题。

不同的虚拟机采用的方案也不尽相同,比如CMS使用的是增量更新,而G1采用的则是原始快照
至于为什么G1为什么选择原始快照,个人觉得和内存的设计分布有关系,再加上从灰色遍历的深度相对从黑色遍历要低一些

需要注意的是,不管采用以上的哪种方式,都不可能完全避免用户线程停顿(STW),这似乎是个无解的问题,就好比既要打扫会场却又允许观众可以自由活动,十块饼干分给3个小朋友这种世界级难题,不可能做到绝对完美…
只是在重复标记阶段要求用户线程暂时挂起,相对于整个gc流程用户线程挂起来看,这个停顿时间已经相对非常小,剩下的优化方向就是如何使gc更轻更快的执行,但是更轻 更快何尝又不是一种矛盾呢

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值