这是why技术的第36篇原创文章html
上面的图片是我上周末在家拍的。之后的文章里面个人第一张配图都用本身随手拍下的照片吧。分享生活,分享技术,哈哈。java
阳台上的花开了,成都的春天快来了,疫情也应该快要过去了吧。web
最近在看《霍乱时期的爱情》,不知道为何和《大话西游》联系了起来,因此你能够看到玻璃上的倒影,是我在看《大话西游》。面试
谁都曾经有过大闹天宫的梦想,爱上层楼的忧愁,可是迟早有一天,你也会像他转身以后同样,走在路上,像一条狗。算法
好了,说回文章并发
上《面试官:你说你熟悉jvm?那你讲一下并发的可达性分析》这篇文章主要聊了 jvm 的可达性分析算法。oracle
借助“三色标记”大法分析了垃圾回收线程扫描的过程当中,用户线程同时执行修改引用关系的操做时,可能会出现的“对象消失”问题,以及其对应的两种解决方案jvm
增量更新和原始快照。jsp
在文章中我写道:对象关系图的变化会致使出现两种状况一是“浮动垃圾”,二是“对象消失”。大几率的状况下面试官更加关心第二种状况,由于第二种状况会给程序带来异常。接下来我就作动图分析了“对象消失”的状况编辑器
可是我是万万没想到呀,读者更关心的是“浮动垃圾”。有的读者就来问我,浮动垃圾是怎么产生的,你却是给个图啊。
像我这样的又暖又有料的硬核原创做者,你说你要,那我确定是要给你的。
下面就给你补上“浮动垃圾”的动图:
当并发标记完成后,对象图就变成了下面这个样子:
你看出来了吧。对象7,8,4,11,10都是浮动垃圾。由于他们被标记成了黑色,因此逃过了本次垃圾回收。
什么?你问我为何黑色就不回收了?你个假粉丝,建议你先去读一读上周的文章。
有的读者就提出了另外的颇有探讨性的问题:
why哥你好,你《面试官:你说你熟悉jvm?那你讲一下并发的可达性分析》这篇文章主要解决了在并发标记阶段,GC线程和用户线程并发执行时,用户线程修改了对象引用关系,致使“对象消失”的问题。G1是采用原始快照加写前屏障的方式解决这个问题的。
可是我还有另外的一个问题:用户线程执行时不只修改了对象引用关系,还新分配了新对象,我以为这个状况是很是常见的,G1是如何找到并处理这些对象的呢?
换句话说,就是文章标题啦:G1收集器是怎么知道这些对象是何时应该进行垃圾标记的?
这是一个好问题,一看就是用心读了文章并带有本身的思考。很不错。
这位读者的问题属于第一个问题的连环炮,让我忽然有了一种掉进了面试官布好的天罗地网里面的感受。
面试官先故意漏出破绽,让你聊“对象消失”、“三色标记”、“增量更新”。而后等你得意洋洋的时候,忽然抛出第二个问题:刚刚对象消失的问题回答的不错,那若是并发标记的时候用户线程分配了新对象,G1是怎么处理的呢?
说实话,我以为只要你简历上没有写精通jvm,面试通常问到这种程度的我以为是真的到了探讨的地步了。答的上来加分,答不上来也不扣分。
遥想2016年,我刚毕业,只身闯北京的时候,一连面试了9家公司,没有一家公司聊到 jvm (固然我当时面的是初级开发)。如今不同了,不知道何时 jvm 从进阶面试题,变成了初级面试题。面试阶段若是没有问 jvm ,就感受不是一次完整的面试。
我以为就这几年面试题的变化,其实也就是反映了一个现象:想入行的人愈来愈多,致使入行的门槛愈来愈高。
不是jvm的地位变了,而是门槛愈来愈高了。
好了,瞎逼逼完了,接下来咱们聊聊G1。
我不知道你是怎么知道G1的,可是我是从周志明大大的《深刻理解Java虚拟机(第2版)》这本书里面第一次知道G1收集器的。
我记得当时读到G1的时候感受这就是天书啊。
由于做者在介绍G1以前介绍了不少其余的收集器,我先给你看一下目录,带你回顾回顾:
能够看到,3.5.1节到3.5.6节介绍的收集器工做的时候, Java 堆的内存布局是按照新生代,老年代进行总体的区域划分的。
可是到了G1收集器, Java 堆的内存布局就有点"妖艳贱货"了。而后就有点愈来愈看不懂了,当时的场景就像下面这样:
它虽然仍是保留的有新生代和老年代的概念,可是新生代和老年代以前不再是区域上的隔离了。它将整个 Java 堆划分为多个大小相等的独立区域,叫作 Region 。而新生代和老年代就是由一个个 Region 动态组成的区域,它们能够是不连续的区间。
每个 Region 均可以根据须要,扮演新生代的 Eden 空间,Survivor 空间,或者老年代空间。除此以外它还有一类特殊的区域叫作 Humongous,专门用来存储大对象。
上面说的是啥意思呢?其实用图片看起来就很是直观了:
好比对于 CMS,使用的堆内存结构以下:
能够看到上面的图片中不管是年轻代、老年代都是逻辑上连续的空间(可是不要求物理上的连续)。
而G1的堆内存被划分为多个大小相等的 Region ,可是 Region 的总个数在 2048 个左右,默认是 2048 。对于一个 Region 来讲,是逻辑连续的一段空间,其大小的取值范围是 1MB 到 32MB 之间。
结构以下:
上面的E、S和没有写字母的蓝色方块(能够理解为old)没啥说的。
可是能够看到H是以往的垃圾收集器中没有的概念,它表明 Humongous,这表示这些 Region 存储的是巨型对象(humongous object,H-obj),当新建对象大小超过 Region 大小一半时,直接在新的一个或多个连续 Region 中分配,并标记为H。
说实话上面的这概念已经“烂大街”了,任何一篇写G1都会聊到,包括本文也是。
没办法啊,朋友们,这是引子,必须得先聊几句。就像斗地主,你第一手牌能直接出王炸吗?不能啊,你不得先来一个对三,按部就班啊。
下面我送你一个小彩蛋吧。
注意到我上面说的几个数据了吗,2048个左右,1MB到32MB,这些数据是哪里来的呢,我说你就信了吗?
不少文章聊到G1的时候都只是说堆内存被划分为多个大小相等的 Region , Region 大小的取值范围为 1MB 到 32MB ,可是并无提到 2048 这回事,我来给你寻根问祖一下:
我找到的第一个数据来源于上面的这篇论文,即文末的资料4:
The goal is to have around 2048 regions for the total heap.
这篇论文的做者是Monica Beckwith,你能够去搜一下,她(是的,我没打错,是个妹子)担任过Oracle G1 垃圾收集器性能团队 Leader,权威吧。
第二个数据来源固然是源码了,更权威吧:
http://hg.openjdk.java.net/jdk/jdk/file/fa2f93f99dbc/src/hotspot/share/gc/g1/heapRegionBounds.hpp
知道这个2048重要吗?我以为不重要。
可是知道了就更牛逼呀!当妹子聊到2048的时候她只知道这是一个游戏,你要告诉她这个数字也是G1的Region的默认个数。
事了拂衣去,深藏功与名。
这一部分,也是耳熟能详的部分,可是忍一忍,立刻就要到你高呼:卧槽,牛逼的部分了。
众所周知,通常咱们说G1的收集过程分为下面这四个步骤(下面四个步骤的描述来自于《深刻理解Java虚拟机(第3版)》):
说实在的,下面的描述确实看的让人很懵逼的。面试的过程当中问到这一部分的时候,我相信大多数朋友都是硬背下来的。
因此,本文的目的就是为了让你理解下面这几个阶段的具体过程。
这么说吧,若是看完这篇文章你仍是没搞懂上面这几个阶段的话,那你再读一遍。
再读一遍,仍是没懂的话,那我这篇文章就算写失败了。
初始标记(Initial Marking):这阶段仅仅只是标记GC Roots能直接关联到的对象并修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确的可用的Region中建立新对象,这阶段须要停顿线程,可是耗时很短。
并且是借用进行Minor GC的时候同步完成的,因此G1收集器在这个阶段实际并无额外的停顿。
并发标记(Concurrent Marking):从GC Roots开始对堆的对象进行可达性分析,递归扫描整个堆里的对象图,找出存活的对象,这阶段耗时较长,可是能够与用户程序并发执行。
当对象图扫描完成之后,还要从新处理SATB记录下的在并发时有引用变更的对象。
最终标记(Final Marking):对用户线程作另外一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少许的 SATB 记录。
筛选回收(Live Data Counting and Evacuation):负责更新 Region 的统计数据,对各个 Region 的回收价值和成本进行排序,根据用户所指望的停顿时间来制定回收计划。
能够自由选择任意多个 Region 构成回收集,而后把决定回收的那一部分 Region 的存活对象复制到空的 Region 中,再清理掉整个旧 Region 的所有空间。
这里的操做涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。
上面虽然有4个阶段,可是从上帝视角,咱们能够把它分为两大部分,或者说从整个算法的角度,咱们能够切分为两大部分:
1.Global Concurrent Marking:全局并发标记。
2.Evacuation Pauses:该阶段是负责把一部分Region里的活对象拷贝到空Region里面去,而后回收本来的Region空间。
为何我敢这样去划分呢?
一部分缘由来自这篇论文中:
《Garbage-First Garbage Collection》这篇论文是 sun 实验室在 2004 年发布的第一篇关于 G1 的论文。够权威吧?
该论文中,2.3小节就是介绍 Evacuation Pauses ,2.5小节就是介绍 Concurrent Marking ,下面是部份内容截图:
另外一部分缘由是 R大 也这样说的(见文末参考资料)。
接下来,要回答读者提出的问题,咱们就须要了解全局并发标记阶段。
这一节就是回答这个问题:用户线程执行的时候不只修改了对象引用关系,还新分配了新对象,G1 是如何找到并处理这些对象的呢?
要回答这个问题,就涉及到 TAMS 了。前面我引用的书里说:
初始标记(Initial Marking):这阶段仅仅只是标记 GC Roots 能直接关联到的对象并修改 TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确的可用的 Region 中建立新对象。
这句话,每一个字都能看懂,连在一块儿读,也品出点儿味道,可是总感受似懂非懂的样子。
什么是 TAMS?什么是正确可用的 Region?新对象是建立在 Region 中的哪一个位置的?
咱们先从论文入手,我捡关键点给你说:
1.有两个 bitmap。
2.一个叫 previous,一个叫 next。
3.previous bitmap 是 concurrent marking 阶段完成后的最后一个 bitmap。(有点绕,后面会解释)。
4.next bitmap 是当前将要或正在进行的 concurrent marking 的结果。
5.当标记完成后,两个 bitmap 会交换角色。
1.标记周期的第一个阶段就是清理 next bitmap。
2.而后,初始标记阶段 Stop The World(后面简称STW),目的是标记 GC Roots 能直接关联到的对象。该阶段借助 Minor GC 完成,没有额外的停顿。
3.每一个 Region 包含两个 TAMS。
4.一个对应前一轮标记,一个对应下一次标记。
从论文中咱们能够知道,G1的Concurrent Marking 用了两个 marking bitmap。
一个 previous Bitmap 记录的是上一轮 Concurrent Marking 后的对象标记状态,由于上一轮已经完成,因此这个bitmap的信息能够直接使用。
一个 next Bitmap 记录的是当前这一轮 Concurrent Marking 的结果。这个bitmap是当前将要或正在进行的 Concurrent Marking 的结果,还没有完成,因此还不能使用。
咱们能够假设一次并发标记变成后的 Bitmap(previous Bitmap) 大概长这样:
白色地址之间是能够回收的对象,灰色地址之间是不能够回收的对象。
除了两个 bitmap 外,还有两个 TAMS(top at mark start)。每一个 Region 都有两个 TAMS,分别是 previous TAMS 和 next TAMS。
bitmap 和 TAMS 能够用下面的图片来表示:
首先咱们能够看到 bottom 和 top 之间是一个 Region 已使用的部分。Top 到 end 以前是一个 Region 未使用的部分。
另外能够看到上面我留了四个问号,接下咱们的目的就是填补这些问号。当这些问号被填上以后,全部的问题都会迎刃而解。
两个 Bitmap 和两个 TAMS 是怎么工做的呢?
接下来按照:
初始标记(Initial Marking)
并发标记(Concurrent Marking)
最终标记(final marking,也叫Remark)
清理阶段(Cleanup)
这四个阶段做图说明
从图片能够看到初始标记阶段 nextBitmap 是清空状态,没有标记任何存活的对象。
接着咱们再次回到书中的描述里,我给你逐字描述清楚:
初始标记(Initial Marking):这阶段仅仅只是标记 GC Roots 能直接关联到的对象并修改 TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确的可用的 Region 中建立新对象。
GC Roots 能直接关联到的对象:就是一个 Region 已经使用过的部分,因此在 Bottom 与 top 之间。
修改 TAMS 的值:就是让此时的 prevTAMS 指向 Bottom ,也就是一个 Region 内存地址起始值。让此时的 nextTAMS 指向 Top。Top 实际上就是一个 Region 未分配区域和已分配区域的分界点。
**正确的可用的 Region **:对一个 Region 来讲,当上面的 nextBitmap 为空、4个指针都准备就绪后,这个 Region 在下一阶段用户程序并发运行时,就是一个正确的 Region。
下一阶段用户程序并发运行时,在正确的可用的 Region 中建立新对象是什么意思呢?
下一阶段用户程序并发运行时指的就是并发标记阶段。
先看前面引用的书中描述:
并发标记(Concurrent Marking):从 GC Roots 开始对堆的对象进行可达性分析,递归扫描整个堆里的对象图,找出存活的对象,这阶段耗时较长,可是能够与用户程序并发执行。当对象图扫描完成之后,还要从新处理 SATB 记录下的在并发时有引用变更的对象。
再看动图:
从 GC Roots 开始对堆的对象进行可达性分析,递归扫描整个堆里的对象图,找出存活的对象:
意思就是说在并发标记阶段, GC 线程工做在 prevTAMS 和 NextTAMS 之间,对堆里的对象进行可达性分析(回想一下“三色标记”),标记完成后, NextBitmap 就有对应有值了(里面放的是地址值),黑色对应的是存活对象,白色对应的垃圾对象。
这样就找出存活对象了。
可是书中并无说起用户线程分配对象的状况。因此读者提出的问题,在书中也找不到明确的答案。
答案就是: NextTAMS 与 Top 之间的对象,就是本次并发标记阶段用户线程新分配的对象,它们是隐式存活的。
为何这么说?你去品一品论文里面我框起来的这句话。
可是面试官想要的是这一句话的答案吗?不是的。
你听到这个问题后,你先微微一皱眉,作出沉思状,而后轻轻说说一句:这个问题问的很好,我先组织一下语言。(先舔他一波)
而后你按照阶段把图画出来,指着给他讲 TAMS 和 Bitmap 是怎么工做的。
另外,关于 NextTAMS 与 Top 为何是重叠的,也得补充说明一下:并发标记的前一个阶段是初始标记。因为初始标记是 STW 的,因此从动图中咱们能够看到:并发标记开始,即初始标记结束的时候, NextTAMS 与 Top 是重叠的。
随着并发标记过程的进行, NextBitmap 被填充上了值。而 NextTAMS 与 Top 之间的区域愈来愈大,这就是用户线程在并发标记阶段分配的新对象。
同时经过下面的图咱们能够看到, GC 线程的工做区间和用户线程的工做区间是有重叠的(用工做区间这个概念去理解其中的一些细节不必定正确,可是能够这样抽象的认为,方便理解)。
而重叠的部分,就是可能产生“对象消失”的部分。对G1来讲,就是原始快照(STAB)加写前屏障(Pre-Wirte Barrier)工做的部分。
因此这就是书里为何说:当 GC 线程扫描完对象图后,还须要从新处理 STAB 记录下的在并发时有引用变更的对象。
书中是这样的写的:
最终标记(Final Marking):对用户线程作另外一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后那少许的 SATB 记录。
最终标记阶段,因为是 STW 的,因此该阶段对应的图是并发标记阶段完成后的图,以下:
处理并发阶段结束后仍遗留下来的最后那少许的 SATB 记录是什么意思呢?
你想,并发标记阶段, GC 线程完成对象图的扫描以后,还会去处理 SATB 记录下的在并发时有引用变更的对象。
在处理 SATB 记录的数据的时候,因为用户线程可能仍是在继续修改对象图,继续在产生 SATB 数据,因此仍是会有一小部分的 SATB 数据,因此才须要一个短暂的暂停。
书里写的是筛选回收阶段。其实就包含了清理阶段和回收阶段。这里咱们只讨论清理阶段,不讨论回收。
在这个阶段, NextBitmap 和 PrevBitmap 会交换位置:
因此,咱们的图就变成了下面的样子:
能够看到,NextBitmap 和 PrevBitmap 交换了位置,NextTAMS 和 PrevTAMS 交换了位置。
而 Region 中, Bitmap 白色部分对应的已使用内存变成了浅灰色。它仅仅是标记了出来,并无进行清扫操做。
须要注意的是:清理阶段不拷贝任何对象
引用R大的回答来描述这个阶段:
清点和重置标记状态。这个阶段有点像 mark-sweep 中的 sweep 阶段,不过不是在堆上 sweep 实际对象,而是在 marking bitmap 里统计每一个 Region 被标记为活的对象有多少。这个阶段若是发现彻底没有活对象的 Region 就会将其总体回收到可分配 Region 列表中。
好了,到这里咱们就能把前面的那张图给填上了:
而后再看一下论文中的这张图片,你就会发现,我上面的过程都是基于这张图片去分析的,图中展现了两个循环, A-B-C , D-E-F 。其中 E、F 过程就是 B、C 过程的重复:
我让上面的图片动起来,请细细品。请注意各个阶段 PrevTAMS 、 NextTAMS 指针的交换、 PrevBitmap 和 NextBitmap 位置的交换:
若是一次看不懂,就再看一次。看的时候结合上面的长图和动图一块儿分析,效果更佳。
参考资料:
1.https://max.book118.com/html/2018/0815/7043143036001143.shtm
2.https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html
3.https://www.oracle.com/technetwork/java/javase/tech/g1-intro-jsp-135488.html
4.https://www.infoq.com/articles/G1-One-Garbage-Collector-To-Rule-Them-All/
5.https://hllvm-group.iteye.com/group/topic/44381
6.《深刻理解Java虚拟机(第三版)》
本文是对《面试官:你说你熟悉jvm?那你讲一下并发的可达性分析》这篇文章的补充说明。若是你没看过,我建议你去看看。
我以为有些知识点仅仅靠文章和图片是很难说清楚的,因此我费劲的作了动图。
为了作这篇文章和上篇文章中的几张动图,加起来我截了 80 多张图。你知道我为了把每张图截的一个像素都不差,我有多努力吗?
截的我眼球布满了血丝,眼睛都快瞎了,你不关注一波?
我四级半的英语水平,为了文章的正确性,强行啃英文论文,你不感动吗?
点个关注呀,别白嫖我啊,大哥。写文章很辛苦的,须要来点正反馈。
才疏学浅,不免会有纰漏,若是你发现了错误的地方,还请你留言给我指出来,我对其加以修改。
感谢您的阅读,我坚持原创,十分欢迎并感谢您的关注。
我是why技术,一个不是大佬,可是喜欢分享,又暖又有料的四川好男人。
以上。
欢迎关注公众号【why技术】,坚持输出原创。分享技术、品味生活,愿你我共同进步。