这是why技术的第37篇原创文章
老规矩,先聊聊生活,上面这张图片是我周一拍的。
周一晚上下班后发现公司楼下推着三轮车卖花的阿姨又开始买花了。整个路口只有她一个人在做生意,整条路上也没有几个人,大家都低着头匆匆走着,繁花中带着点忧伤。
于是,我去买了一把白玫瑰。
上周日把《霍乱时期的爱情》看完了,就刚好当道具拍了上面的照片。总体来说我不喜欢这种纵情声色的故事,更不喜欢那个看起来冠冕堂皇的理由∶“我一生有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?那你讲一下并发的可达性分析》中对于这一部分的描述过于简单,且存在错误,给大家道歉,并特以此文进行修正。
你是什么时候的垃圾-勘误
在《G1回收器:我怎么知道你是什么时候的垃圾?》这篇文章中有一句描述是这样的:
“GC Roots 能直接关联到的对象:就是一个 Region 已经使用过的部分,所以在 bottom 与 top 之间。”这句话是错误的。
实际上,通过文章后面的描述你也能发现。GC Roots 能直接关联到的对象集合应该“小于” Region 已经使用过的部分,对象图递归完之后,所有对象总和,才等于Region已经使用过的部分。
通过文章中后半部分的这个图片也可以直观的发现, bottom 到 top 之间是一个 Region 已经使用的部分。但是这一部分中,只有 bottom 到 NextTAMS 之间的对象才是 GC Roots 能直接关联到的对象,这部分对象并不是一个 Region 已经使用过的部分。
你是什么时候的垃圾-补充说明
关于《G1回收器:我怎么知道你是什么时候的垃圾?》这篇文章,还有两个需要补充说明的地方。
有的读者问说:文章中没有讨论回收的内容,每次清理不会真正回收,那是不是多轮标记后才发生一次回收呢?
一。
首先,文章中确实没有讨论回收相关的内容。我在前面部分也写了,把G1回收切分为两大部分:
1.Global Concurrent Marking:全局并发标记。
2.Evacuation Pauses:该阶段是负责把一部分Region里的活对象拷贝到空Region里面去,然后回收原本的Region空间。
只要清楚了全局并发标记阶段,就可以解答文中抛出的这个问题:
所以我只说明了全局并发标记阶段。
如果想要了解回收阶段的事,可以去看看R大的回答,强烈建议你看完本文,点个赞后,打开下面的链接,反复阅读几遍:
https://hllvm-group.iteye.com/group/topic/44381
其次,“每次清理不会真正回收,那是不是多轮标记后才发生一次回收呢?”
这句话,可能是我在文章强调了清理阶段不拷贝任何对象,再加上没有描述回收阶段,导致读者有点懵了吧。
一次全局并发标记完成后,紧接着一次回收的过程。
只是G1收集器之所以能建立可预测的停顿时间模型(-XX:MaxGCPauseMillis指定,默认值为200毫秒),是因为它将 Region 作为单次回收的最小单元,即每次收集到的内存空间都是 Region 大小的整数倍,这样就可以有计划地避免在整个Java堆中进行全区域的垃圾回收。
更具体一点的做法就是每个 Region 里面堆积的垃圾都有一个“价值”(价值即回收所获得的空间大小以及回收所需要的时间的经验值)。而这些“价值”,是维护在一个优先级列表中的,G1收集器都是知道的。
所以回收阶段会优先处理回收价值最大的那些 Region。因此,一次回收的过程并不会回收所有的 Region。
二。
这里也就解释了读者提出的另外一个问题:如果每次标记完都会回收整理,那为什么红框所在的区间与上一次标记之后相同,好像没有被整理一样,整理之后不是应该不留下内存空隙吗?
我觉得一个合理的解释,就是我上面说的:这个 Region 的价值不够,所以它本次没有被回收。随着时间的推移,它里面堆积的垃圾越来越多,“价值”就越来越高,总是会被回收的。
还有读者问:看了并发标记的过程,有个疑问 prevBitmap 的作用是什么? 因为感觉每次都是从头开始扫描,没看到它的作用。
这个问题,可以从这张图片入手解答:
这个 E 是 Remark 阶段,可以看到,在这个阶段,其实 PrevBitmap 是派上用场了。
前面刚刚说了,这个 Region 由于“价值”不够,它逃过了上次垃圾回收,所以待到下次垃圾回收的时候,就是 prevBitmap 的用武之地了,它里面记录的地址对应的区间就不需要再次标记了,因为这些地址对应的对象就已经是垃圾了。
我们可以假设 E 代表的是第 n 轮回收的过程的Remark阶段。那么 PrevBitmap 就是第 n-1 轮的标记结果。
之前的文章说了:一个 previous Bitmap 记录的是上一轮 Concurrent Marking 后的对象标记状态,因为上一轮已经完成(上一轮就是第n-1轮),所以这个bitmap的信息可以直接使用。
可以直接使用的意思就是前面说的:它里面记录的地址对应的区间就不需要再次标记了,因为这些地址对应的对象就已经是垃圾了。
到 F 图里面,可以看到,当前的 F 图是清理阶段已经完成的状态了:
判断标准有二:
1.和 E 图相比PrevBitmap 和 NextBitmap 已经交换了位置。
2.PrevBitmap 里面对应的地址的空间已经被标记为浅灰色了。
这个时候已经完成标记,PrevBitmap 又变成了第n-1次标记的结果。
你是什么垃圾-怼人
因为之前的文章已经发布了,所以我需要修改一下对应的内容。提醒后面的读者,如果看到了文章,需要注意这些地方描述的有问题。
但是我在查找我文章的过程中发现了一些让我很郁闷的事情,之前的文章,大都被剽窃了,我也见怪不怪,有时间就顺手举报一下了。
最过分的是下面这个:
这是一个百家号账号,一字不差的抄我文章,还自己标注为“原创”?
我去写了个评论:
他还不敢把评论放出来。
还有下面这个,你可长点心吧。你配的这张图片,我倒是想在家拍,但是我拍不出来呀:
这样的情况还有很多。说到底,就还是版权意识的问题。
版权问题,我之前在《订阅号做了77天,我挣了487.52元》这篇文章里面聊过:
我的号不会传播任何盗版资源,以前如此,现在如此,以后也会如此。
不做恶,就是最大的善。与君共勉。
所以我在此郑重声明,如果未经许可转载我的文章,必须标明原文地址,且保留文末公众号二维码,否则我一定见一个举报一个。
我先举报你涉黄,引起工作人员的注意,再举报你抄袭,让工作人员惩罚你。
气死我了。
最后说一句(求关注)
通过这件事我也再次感觉到了,看网上的野生文章(比如我的),要持有谨慎、怀疑、学习的态度。
才疏学浅,难免会有纰漏,如果你发现了错误的地方,还请你留言给我指出来,我对其加以修改。(我每篇技术文章都有这句话,我是认真的说的。)
感谢您的阅读,我坚持原创,十分欢迎并感谢您的关注。
我是why技术,一个不是大佬,但是喜欢分享,又暖又有料的四川好男人。
以上。
欢迎关注公众号【why技术】,坚持输出原创。分享技术、品味生活,愿你我共同进步。