老规矩,先聊聊生活,上面这张图片是我周一拍的。
周一晚上下班后发现公司楼下推着三轮车卖花的阿姨又开始买花了。整个路口只有她一个人在做生意,整条路上也没有几个人,大家都低着头匆匆走着,繁花中带着点忧伤。
于是,我去买了一把白玫瑰。
上周日把《霍乱时期的爱情》看完了,就刚好当道具拍了上面的照片。总体来说我不喜欢这种纵情声色的故事,更不喜欢那个看起来冠冕堂皇的理由∶“我一生有622个情人,但是我只爱过你”。虽然它真的是穷极了爱情的所有可能性,但是它不够真实。
相比之下我觉得钱钟书先生写的《围城》∶“我说的让她三分,不是三分流水七分尘的三分,而是天下明月只有三分的三分。”这样打打闹闹的爱情更加真实。
再看杨绛先生的《我们仨》,书的最后她说∶“世间好物不坚牢,彩云易散琉璃脆”。这才是爱情,这才是真实的生活。
好了,说回文章。
对不起,我错了。
前面发的这两篇文章:
里面有一些没有说清楚的地方,又有很多读者来问,所以我觉得需要补充说明一下。
更重要的是,经过高手指点,其中还有一些描述错误的地方,我也需要进行勘误。
如果真的是面试题,可能面试官就会对我说:好了,我们今天就先到这里。你回去等通知吧。
如果你没看过我刚刚说的两篇文章,我建议你不要看这篇,因为一看就得看三篇,如果里面的衍生知识点你还想彻底弄明白,一个下午就过去了......(当然,你看了后收获肯定还是有的。)
如果你看了我之前的两篇文章,我求求你一定看看这篇,补充、更正一下答案,等面试官真的问起细节来,也不怕......
好了,在阅读本文之前,我假设你已经读过我前面说的两篇优质、幽默、有料的文章了。
并发的可达性分析-勘误
之前发布了这篇文章《面试官:你说你熟悉jvm?那你讲一下并发的可达性分析》,对于文中这一部分内容中的动图,有很多朋友给我说看不懂:
我把这个动图拿出来:
首先,需要说明的是,我现在也看不懂这个动图了。(画错了就是画错了,还强行找个理由)。
接下来,忘记这个动图,我们重新分析一波原始快照方案(以下简称SATB,Snapshot At The Beginning)。
首先,我们看初始标记阶段(即根节点枚举)完成后,刚刚进入并发标记阶段,GC 线程开始扫描时的对象图:
在上面这张图里,当GC Roots确定后,对象图就已经确定了。SATB扫描的时候基于已经确定的对象图(快照版的对象图)扫描,也就是说扫描过程中上面的快照图的引用关系是不会发生变化的,但是真实的对象图是会发生变化的。
举个例子:就类似于你在操场上拍了一张照片,你数照片里面的人数,照片是不会发生变化,人数一直都是这么多,但是真实的操场上的人是在时刻变化的。
所以,在对象图确定的一刻,正常扫描完成后,对象图变成了下面这样:
好了,面前的铺垫完成了。
我们这里需要演示的是“对象消失”情况。
首先,我们先确定一下上面展示的对象图,在并发标记阶段必然有一个时刻的对象图是这样的:
我们基于这个时刻的这个对象图去讨论“对象消失”的问题。
还得记得"对象消失"必须同时满足的两个条件吗?(这两个条件是摘抄自《深入理解Java虚拟机(第3版)》P.89)
条件一:赋值器插入了一条或者多条从黑色对象到白色对象的新引用。
条件二:赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。
我们再仔细的读一遍第二个条件,你会发现,它说的是**“该白色对象”。这个“该白色对象”指的是条件一里面的白色对象。**
所以,我们有理由相信:条件一和条件二是有先后顺序的,即必须是赋值器插入了一条或者多条从黑色对象到白色对象的新引用,然后赋值器又删除了全部从灰色对象到该白色对象的直接或间接引用。在这样的情况下,才会出现“对象消失”的情况。
经过高人指点,我们还可以进行反证法,如下:
我们假设灰色对象到白色对象的引用先删除了,即先触发了条件二。那么对应的这个时刻真实的对象图将变成下面的样子
(注意我这里强调的是真实的对象图,而不是快照的对象图。再次重申:快照的对象图在扫描开始的时候就确定了,扫描过程中是不会变化的。)
那么,白色对象9是处于游离态的,从根节点没有任何引用链相连,用图论的话来说就是从 GC Root 到对象9不可达,则证明此对象是不可能再被使用的。因此用户线程不可能把黑色对象5指向游离态的白色对象9,你写不出这样的代码来。
如果说上面的图你一眼没看出来,那么请看下面这图,是不是恍然大悟:
黑色对象5不能指向白色对象9,那么第一条规则就满足不了了。
所以,综上我们可以得出:条件一和条件二是有先后顺序的。
那么我们根据条件一继续做图如下:
条件一是赋值器插入了一条或者多条从黑色对象到白色对象的新引用。
在上面这个图的场景中,就是 GC 线程在工作的同时,赋值器插入了一条黑色对象5到白色对象9之间的新引用。(用红色线条以示区分)
在这个时刻,由于灰色对象6指向白色对象9,所以黑色对象5可以指向白色对象9,想一想我们前面的证明,只要有引用链,黑色对象就可以到达白色对象。
这个时候仅仅满足了条件一,对象还没消失。
接下来就是条件二的图,STAB破坏的就是条件二:
条件二是赋值器删除了全部从灰色对象到该白色对象的直接或间接引用。
在上面这个图的场景中,就是赋值器删除了灰色对象6到白色对象9的直接引用。
这个时候白色对象9就是“消失的对象”了,因为黑色的对象5是不会被再次扫描的。
需要注意的是,赋值器可以理解为用户线程,由于在并发标记阶段,用户线程和 GC 线程在同时运行,所以需要出现上面的图,还有一个前置条件就是:
用户线程删除对象6到对象9之间的引用,要先于 GC 线程扫描到对象6,把对象6变成灰色的操作。因为只有这样,GC 线程处理到对象6的时候,才有对应的写屏障记录。
如果在 GC 线程已经扫描过对象6,即对象6已经是黑色的情况下(这个时候对象9,不是黑色就是灰色,不可能是白色),用户线程再去删除对象6到对象9之间的引用,GC 线程是不需要处理的,因为对象9已经是非白了,它在本轮中必定会活下来。
这里我引用R大的描述:
https://hllvm-group.iteye.com/group/topic/44381?page=2
因为删除操作会触发 pre-write barrier,把每次引用关系变化时旧的引用值记下来,只有这样,等 GC 线程到达某一个对象时,这个对象的所有引用类型字段的变化全都有记录在案,就不会漏掉任何在快照图里活的对象。当然,很可能有对象在快照中是活的,但随着并发 GC 的进行它可能本来已经死了,但 SATB 还是会让它活过这次 GC,变成了浮动垃圾。
SATB 在写屏障里,把旧的引用所指向的对象都变成非白的(已经黑灰就不用管,还是白的就变成灰的)。
这样做的实际效果是:如果一个灰对象的字段原本指向一个白对象,但在concurrent marker能扫描到这个字段之前,这个字段被赋上了别的值(例如说null),那么这个字段跟白对象之间的关联就被切断了。SATB write barrier保证在这种切断发生之前就把字段原本引用的对象变灰,从而杜绝了上述条件二的发生。
其中:“把旧的引用所指向的对象都变成非白的。”在我们这个场景下含义如下:
旧的引用指的是:灰色对象6到白色对象9之间的引用。
所指向的对象指的是:白色对象9。
都变成非白的:指的是白色对象9变成了灰色。
所以,在两个条件顺序触发、对象图扫描完成后会变成下面的样子:
并发扫描结束之后,再以灰色对象9为根(把它作为根,自然会变成黑色),重新扫描一次,所以最终的对象图变成了这样:
有的小伙伴就会问了:如果在标记过程中,用户线程并没有把对象5指向对象9的操作,仅仅是发生了删除对象6到对象9之间引用的操作,那么这个对象图是什么样子呢?
就是下面这个样子,你应该可以想象出来:
对象9还是黑色,只是它变成了浮动垃圾,逃过了本次回收而已。并不影响程序运行。
接下来,让上面的图动起来,并且我把图片之间的切换顺序放慢。你再自己细品品:
所以,上面的全部描述,才是一次我认为正确的,展示SATB方案是如何解决“对象消失”问题的过程。
之前《面试官:你说你熟悉jvm?那你讲一下并发的可达性分析》中对于这一部分的描述过于简单,且存在错误,给大家道歉,并特以此文进行修正。