gc增量更新和原始快照还不懂? 今天就把它搞懂吧!

586 阅读7分钟

前言

在阅读深入理解java虚拟机一书的3.4.6 并发的可达性分析章节中, 我们发现作者对于增量更新原始快照的描述模棱两可

当时并没有很关注这点, 今天发现一到面试题居然细到这种程度, 为了在面试官面前装次AC, 今天我们就彻底搞懂原始快照和增量更新到底是什么?

不过在了解这些之前, 我们先要总结下书本上的一部分知识点, 否则各位读者都不懂本篇文章到底描述了什么问题, 解决了什么问题

CMS的执行步骤

  1. 标记gc roots根节点(STW)
  2. 并发标记gc roots下面的所有节点
  3. 重新标记(STW)
  4. 并发清除

这里我们关注并发标记gc roots下面的所有节点

三色标记(Tri-color Marking)

重点知识: 帮助我们了解可达标记的过程

我们引入三种颜色

  • 黑色
  • 灰色
  • 白色

这些颜色用于标记gc roots中的个个对象节点

  • 黑色: 意味着这个对象可达

  • 灰色: 这个对象可达, 并且是当前标记的进度

  • 白色: 对象不可达, 或者对象并未被标记到

而可达性标记的过程中, 我们可以想象为声波在空气中传递的过程, 这里假设我们是养鸡的养殖户, 现在要叫山上的一群鸡吃饲料

看这个画面更加生动形象:

这里我们关注的是声波, 听到声音的鸡是黑色, 刚刚听到声音的鸡是灰色, 还未听到声音的鸡是白色

最后还未过来吃饭的白鸡将被gc清除掉

虽然例子好理解些, 但是根本核心没讲清楚:

首先正常情况下

  • 黑色要么指向黑色, 要么指向灰色

  • 白色只能被灰色或者白色指向

如果出现其他情况, 可能导致一些问题

那么这个过程有存在什么问题么?

我们需要知道, 一次gc的时间是很久的, 可能有几十到几百ms, 如果是full gc 更久, 所以这个过程中会产生很多新的对象, 也会有很多对象变为不可达对象

如果在一次gc并发标记的过程中出现 我们new了一个新的节点, 默认被标记为白色, 然后我们使用黑色节点的set方法, 将白色节点set进黑色节点的字段中

黑色.set(new 白色())

此时 黑色节点 ==> 白色节点

这中间并没有灰色节点意味着扫描过去了, 我们新创建的白色节点将在此时gc结束后被清除掉

这就非常严重了

原书中这张图更加形象:

image.png

换句话说, 不论对象是什么状态, 只要插入到灰色对象之后(被白色或者灰色对象引用), 就一定不会标记错误, 比如插入到上图中的任何白色对象之后, 它不会出现肉眼可见的明明可达, 但是在该次gc中被清除的情况

而如果白色对象被黑色对象引用后明明肉眼可见可达, 但是却失去了这次gc标记的机会, 可能就被清除, 比如下面这个情况

image.png

怎么解决这个问题

我们分析上面两张图, 会发现他们有个共性:

  • 黑色指向白色对象
  • 该白色对象失去了本次被标记的机会, 也就是说灰色对象失去了直接或者间接能够标记到该对象的机会

同时满足这两种情况, 才会导致有用对象在本次gc结束后新创建的节点被删除的问题

小白: 即便这个对象被标记为不可达, 最后不是有finalize方法会发现它可达么? 为什么还需要纠结这个问题?

小黑: 首先finalize方法不稳定, 执行时间不确定, 效率也不行, 另外最重要的问题是finalize方法不会无限的救你, 只会在第一次执行finalize方法的时候才会发现你可达, 如果再次出现不可达问题finalize方法可能都不会被调用了

要解决这个问题, 我们只要破坏这两种条件的其中一种即可

jvm给出了两种方法

  • 原始快照
  • 增量更新

image.png

增量更新

增量更新要破坏的是第一个条件,当黑色对象插入新的指向白色对象的引用关系时,就将这个新插入的引用记录下来,等并发标记结束之后,再将这些记录过的引用关系中的黑色对象为根,重新标记一次。这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它(黑色)就变回灰色对象了。

例子

增量更新就好像你每次打电话时,将确认过的地址信息记录在你的手机备忘录里。当所有包裹都送达后,你会再次检查备忘录中的地址信息,并与实际情况进行对比。这样,即使有发件人或收件人在你记录完地址后更改了地址,通过第二次检查(重新标记阶段),你仍然能够发现并纠正错误。

原始快照

原始快照要破坏的是第二个条件,当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发标记结束之后,再将这些记录过的引用关系中的灰色对象为根,重新标记 一次。这也可以简化理解为,无论引用关系删除与否,都会按照刚刚开始标记那一刻的对象图快照来进行搜索。

需要注意的就是这个过程中存在两个阶段

  • 并发标记
  • 重新标记

怎么理解这句话?

原始: 是指在白色节点脱离灰色节点时

快照: 是指镜像, 或者平行时空, 也可以说是复制品

合并起来, 原始快照的意思就是在白色节点脱离灰色节点前, 记录灰色节点指向白色节点的这个关系

记录完毕后, 白色节点彻底脱离灰色节点, 此时白色节点在并发标记阶段这个过程中无法被标记, 这样如果白色节点还有引用也将被认为是垃圾节点

此时在重新标记阶段这个过程中, gc将会把先前记录到的灰色节点指向白色节点的这个快照拿出来, 从灰色节点开始可达标记

这样即便实际的白色节点变为垃圾, 那也是下次gc垃圾收集的事情, 本次重新标记阶段是在快照中的灰色和白色节点

不过这样会导致实际的白色节点变为浮动垃圾

举个例子

原始快照则类似于你在出发前,拍了一张包裹堆放整齐、标注清晰的照片。然后,在整个运输过程中,无论包裹堆放顺序是否发生变化,你都按照照片上的记录进行送货。这样,即使有包裹在运输过程中被重新堆放,你也能按照最初的记录进行处理,不会遗漏任何一个包裹。

这里需要注意的就是原始快照不会管你中途的变化, 它都是按照快照上的记录进行可达标记, 有点类似于写时复制, 将原先的关系复制下来, 丢给重新标记阶段