上图是上周末在家拍的。在以后的文章中,第一张图都会用自己随手拍的照片。分享生活,分享技术,哈哈。阳台花开了,成都春天来了,疫情应该快结束了。最近在看《霍乱时期的爱情》,不知道为什么和《大话西游》连在一起,所以你能看到玻璃上的倒影,是我在看《大话西游》。每个人都曾有过闹上天堂的梦想,也有过爱上楼上的心酸,但迟早有一天,转身后的你也会像他一样,走在路上,如狗一般。好了,回到文章。上周给大家看“漂浮垃圾”《面试官:你说你熟悉jvm?那你讲一下并发的可达性分析》这篇文章主要讲了jvm的可达性分析算法。借助“三色标记”方法,垃圾回收线程扫描过程中,用户线程同时执行修改引用关系操作时可能出现的“对象消失”问题,及其对应的两个解决方案增量更新和原始快照。我在文章中写道:对象关系图的改变会导致两种情况:一种是“漂浮的垃圾”,另一种是“对象消失了”。大概率面试官更关心第二种情况,因为第二种情况会给程序带来异常。接下来我做了一个动画来分析“物体消失”的情况。但万万没想到,读者更关注“漂浮垃圾”。有读者来问我漂浮垃圾是怎么产生的,不过你可以给我一张图。像我这样一个有温度有信息量的硬核原作者,你说要,那我一定给你。下面给大家放个“漂浮垃圾”的动图:当并发标记完成后,对象图变成这样:大家可以看到了。对象7、8、4、11、10都是漂浮垃圾。因为它们被标记为黑色,所以它们逃脱了垃圾收集。什么?你问我为什么黑色不回收?你这个假粉丝,我建议你先看看上周的推文。G1垃圾回收时如何处理新对象?有读者又提出了一个很有探索性的问题:兄弟你好为什么,你《面试官:你说你熟悉jvm?那你讲一下并发的可达性分析》这篇文章主要解决GC线程和用户线程并发标记的问题在执行过程中,用户线程修改对象引用关系,导致“对象消失”问题。G1通过在原始快照上添加一个预写屏障来解决这个问题。但是我还有一个疑问:用户线程在执行的时候,不仅修改了对象的引用关系,还分配了新的对象。我认为这种情况很常见。G1如何找到并处理这些对象?换句话说,就是文章的标题:G1收集器如何知道什么时候应该将这些对象标记为垃圾?这是一个很好的问题。乍一看,仔细阅读了文章,有了自己的思考。很不错。读者的问题属于第一个问题的系列,让我顿时感觉自己掉进了面试官的罗网中。面试官先是故意泄露漏洞,让你说说“对象消失”、“三色标记”、“增量更新”。然后得意之余,突然抛出第二个问题:刚才对象消失的问题回答的很好,那么如果用户线程在并发标记的时候分配了一个新的对象,G1是怎么处理的呢?说实话,我觉得只要你简历上不写jvm熟练程度,如果面试一般问到这个水平,我觉得真的是时候讨论一下了。答对即可获得积分,答错不扣分。回想2016年,刚毕业一个人去北京,一连面试了9家公司,没有一家公司说jvm(当然,我面试的是初级开发)。现在不同了。不知道什么时候jvm从高级面试题变成了初级面试题。如果面试阶段不问jvm,感觉就不是一个完整的面试。我觉得这几年面试题的变化其实反映了一个现象:想进入这个行业的人越来越多,从而导致进入门槛越来越高。不是jvm的地位变了,而是门槛越来越高了。好了,说完了,说说G1吧。第一次接触GarbageFirst(G1)的时候,不知道大家是怎么知道G1的,我是从周志明的《深入理解Java虚拟机(第2版)》这本书开始了解到G1收集器的。记得当初看G1的时候,感觉就是一本圣经。因为作者在介绍G1之前介绍了很多其他的收集器,先给大家看一下目录,带大家回顾一下:可以看到3.5.1到3.5.6小节介绍的收集器工作时,Java堆内存layout是按照newgeneration和oldgeneration来划分的。但是说到G1收集器,Java堆的内存布局就有点“骚”了。然后就越来越看不懂了。当时的场景是这样的:虽然还是保留了新生代和老年代的概念,但是新生代和老年代已经不再是区域隔离的了。它将整个Java堆划分为多个大小相等的独立区域,称为Region。新生代和老年代是由Region动态组成的区域,可以是不连续的区间。每个Region可以根据需要作为新生代的Eden空间,Survivor空间,或者老年代空间。此外,它还有一种特殊类型的区域,叫做Humongous,专门用来存放大件物品。以上是什么意思?实际的图看起来很直观:比如对于CMS,使用的堆内存结构如下:可以看到上图中的新生代和老年代都是逻辑上连续的空间(但不需要物理连续).G1的堆内存被划分为多个大小相等的Region,但是Region的总数大约为2048个,默认为2048个。对于一个Region来说,是逻辑上连续的一段空间,其大小从1MB到32MB。结构如下:图片来源——文末素材4上面的E、S,还有没有字母的蓝色方块(可以理解为old)没啥好说的。但是可以看出H是一个概念,在之前的垃圾回收器中是不存在的。它代表Humongous,意思是这些Region存储了巨大的对象(H-obj)。在一个或多个新的连续Region中分配,并标记为H。说实话,上面的概念已经是“badstreet”了,任何一篇关于G1的文章,包括本文都会讨论。没办法,朋友们,这是介绍,还是得先聊两句。就像游戏一样,能不能第一手直接玩kingbomb?不,你不必先打一对三,一步一步来。让我给你一个小彩蛋。有没有注意到我上面提到的几个数据,大约2048,1MB到32MB,这些数据是从哪里来的?当我说的时候你相信我吗?很多文章在讲G1的时候只说堆内存是划分为多个大小相等的region,Regionsize的取值范围是1MB到32MB,但是并没有提到2048。请教各位大佬:我找到的第一个数据来自上面的论文,也就是论文末尾的信息4:目标是让totalheap有2048个region左右。这篇论文的作者是MonicaBeckwith,大家可以去搜一下,她(是的,我没看错,是女生)曾担任OracleG1GarbageCollectorPerformanceTeamLeader,权威。第二个数据来源当然是源码,比较权威的:http://hg.openjdk.java.net/jdk/jdk/file/fa2f93f99dbc/src/hotspot/share/gc/g1/heapRegionBounds.hpp知道的这个2048有关系吗?我认为这不重要。但如果你知道它就更棒了!妹子说到2048的时候只知道是游戏,你要告诉她这个数字也是G1默认的Region数量。事毕拂衣,深藏功名。G1的工作步骤部分也是耳熟能详的部分,忍一忍,很快就会到你喊:靠,牛逼的部分。众所周知,一般我们说G1的采集过程分为以下四个步骤(以下四个步骤的描述来自《深入理解Java虚拟机(第3版)》):说实话,下面的描述真的很迷惑。面试的时候问到这部分的时候,相信大部分朋友都是死记硬背的。因此,本文的目的就是让大家了解后面几个阶段的具体过程。这么说吧,如果看完这篇文章上面的阶段你还是不明白,那就再读一遍。如果你再看一遍还是不明白,那我这篇文章的写就算是失败了。InitialMarking:这个阶段只是标记GCRoots可以直接关联到的对象,修改TAMS(NextTopatMarkStart)的值,让下一阶段的用户程序可以在正确可用的RegionCreate中并发运行中间一个new对象,这个阶段需要暂停线程,但是时间很短。而且,它是在MinorGC期间同步完成的,所以G1收集器在这个阶段实际上并没有任何额外的停顿。ConcurrentMarking:从GCRoots开始分析堆中对象的可达性,递归扫描整个堆中的对象图,找出存活的对象。这个阶段耗时较长,但可以和用户程序并发执行。对象图扫描完成后,需要重新处理SATB记录的并发时发生引用变化的对象。最终标记:用户线程上的另一个短暂暂停,用于处理并发阶段结束时留下的最后几条SATB记录。筛选恢复(LiveDataCountingandEvacuation):负责更新Region的统计数据,对每个Region的恢复值和成本进行排序,并根据用户预期的停顿时间制定恢复计划。你可以自由选择任意数量的Region组成一个回收集合,然后将你决定回收的那部分Region的存活对象复制到一个空Region中,然后清理掉整个旧Region的所有空间。这里的操作涉及存活对象的移动,必须挂起用户线程,由多个收集器线程并行完成。虽然上面有4个阶段,但是从上帝的角度,我们可以把它分成两部分,或者从整个算法的角度,我们可以把它分为两部分:1.GlobalConcurrentMarking:全局并发标记。2.EvacuationPauses:这个阶段负责将Region中的一些存活对象复制到空Region中,然后回收原来的Region空间。为什么我敢这样分?部分原因来自这篇论文:《Garbage-First Garbage Collection》这篇论文是SunLab在2004年发表的第一篇关于G1的论文,是否足够权威?本文2.3节介绍了EvacuationPauses,2.5节介绍了ConcurrentMarking。以下是部分内容截图:另一部分原因是R大学也这么说(见文末参考资料)。接下来,为了回答读者的问题,我们需要了解一下全局并发标记阶段。globalconcurrentmarking这一节就是回答这个问题:用户线程执行的时候,不仅修改了对象的引用关系,还分配了新的对象。G1如何找到并处理这些对象?要回答这个问题,就涉及到TAMS了。前面引用的书上说:InitialMarking:这个阶段只是标记GCRoots可以直接关联到的对象,修改TAMS(NextTopatMarkStart)的值,让下一阶段的用户程序可以并发运行,在正确的可用区域中创建新对象。这句话,每一个字都能看懂,一起读,稍微尝一尝,但总感觉半懂半懂。什么是TAMS?什么是正确可用的区域?在Region中创建的新对象在哪里?让我们从论文开始。我说一下重点:1.有两个位图。2.一个叫previous,一个叫next。3、前一个位图是并发标记阶段完成后的最后一个位图。(有点绕,后面会解释)。4.下一个位图是将要或当前正在进行的并发标记的结果。5.标记完成后,两个位图将互换角色。1.标记周期的第一阶段是清理下一个位图。2.然后,初始标记阶段StopTheWorld(以下简称STW),目的是标记GCRoots可以直接关联到的对象。这个阶段是通过MinorGC完成的,没有额外的停顿。3.每个区域包含两个TAMS。4、一个对应上一轮打分,一个对应下一轮打分。从论文中我们可以知道,G1的ConcurrentMarking使用了两个标记位图。前一个Bitmap记录了上一轮ConcurrentMarking之后对象的标记状态。因为上一轮已经完成,所以可以直接使用这张位图的信息。下一个Bitmap记录本轮ConcurrentMarking的结果。这个位图是ConcurrentMarking即将或者正在进行的结果,还没有完成,所以还不能使用。我们可以假设Bitmap(之前的Bitmap)经过并发标记更改后是这样的:白色地址之间的对象可以回收,灰色地址之间的对象不能回收。除了两个位图之外,还有两个TAMS(顶部标记开始)。每个Region有两个TAMS,即previousTAMS和nextTAMS。Bitmap和TAMS可以用下图表示:首先,我们可以看到bottom和top之间是一个Region的used部分。FromToptoend是Region未使用的部分。另外大家可以看到我在上面留了四个问号,我们的目的就是要填这些问号。把这些问号填上,一切问题就迎刃而解了。两个Bitmap和两个TAMS是如何工作的?按照以下步骤进行:InitialMarkingConcurrentMarkingFinalMarking(也叫Remark)清理阶段(Cleanup)绘制的这四个阶段说明:初始标记(InitialMarking)从图中可以看出,nextBitmap处于初始标记阶段被清除,并且没有幸存的对象被标记。那我们再回到书上的描述,我就一字不漏地给大家描述一下:InitialMarking:这个阶段只是标记GCRoots可以直接关联的对象,修改TAMS(NextTopatMarkStart)值,以便用户程序在下一阶段并发运行时,可以在正确的可用Region中创建新的对象。GCRoots可以直接关联的对象:它是一个已经被使用过的Region的一部分,所以在Bottom和Top之间。【勘误见《面试官:你回去等通知吧!》】修改TAMS的值:让此时的prevTAMS指向Bottom,即一个Region内存地址的起始值。此时让nextTAMS指向Top。Top其实就是一个Region的未分配区域和已分配区域的分界点。CorrectavailableRegion:对于一个Region,当上面的nextBitmap为空,并且4个指针都准备好了,这个Region在用户程序下一阶段并发运行时就是一个正确的Region。当用户程序在下一阶段并发运行时,在正确的可用Region中创建一个新对象意味着什么?用户程序并发运行的下一个阶段是指并发标记阶段。ConcurrentMarking先看了前面引用的书里的描述:ConcurrentMarking:从GCRoots开始分析堆中对象的可达性,递归扫描整个堆中的对象图,找出存活的对象,这个阶段需要一个时间长,但可以与用户程序并发执行。对象图扫描完成后,SATB记录的并发时有引用变化的对象需要重新处理。再看动图:从GCRoots开始分析堆中对象的可达性,递归扫描整个堆中的对象图,找出存活的对象:说明在并发标记阶段,GC线程工作在prevTAMS和NextTAMS期间,对堆中的对象进行可达性分析(回忆一下“三色标记”)。标记完成后,NextBitmap会有对应的值(地址值放在里面),黑色对应存活对象,白色对应垃圾对象。这将找到幸存的对象。但是书中并没有提到用户线程分配对象的情况。因此,读者提出的问题,书中找不到。答案是:NextTAMS和Top之间的对象是用户线程在本次并发标记阶段新分配的对象,它们是隐式存活的。你为什么这么说?去尝尝我在一品论文中陷害的那句话。但是这句话的答案是面试官想要的吗?不是。你听到这道题后,微微皱眉,假装若有所思,然后轻声说道:这道题很好,我先整理一下语言。(先舔他)然后你根据舞台画图,指着他告诉他TAMS和Bitmap是怎么工作的。另外,至于为什么NextTAMS和Top重叠,我还要补充说明一下:并发标记的前阶段是初始标记。由于初始标记为STW,从动图可以看出:在并发标记开始,即初始标记结束时,NextTAMS和Top重叠。随着并发标记过程的进行,NextBitmap被填充值。NextTAMS和Top之间的区域越来越大,这是用户线程在并发标记阶段分配的新对象。同时,我们从下图可以看出,GC线程的工作范围和用户线程的工作范围是重叠的(用工作范围的概念来理解一些细节可能不正确,但是它为了便于理解,可以抽象地考虑)。重叠部分是可能发生“对象消失”的部分。对于G1来说,就是原始快照(STAB)加上预写屏障(Pre-WirteBarrier)部分的工作。所以这就是为什么书上说:GC线程扫描对象图后,需要重新处理并发时STAB记录的有引用变化的对象。最终标记(Remark)书是这样写的:最终标记(FinalMarking):在用户线程上再做一次短暂的暂停,处理并发阶段结束后剩下的最后几条SATB记录。最后的标记阶段,由于是STW,所以这个阶段对应的图就是并发标记阶段完成后的图,如下:处理并发阶段结束后剩下的最后几条SATB记录是什么意思?你想,并发标记阶段,GC线程扫描完对象图后,还会处理并发时SATB记录的有引用变化的对象。在处理SATB记录的数据时,由于用户线程可能会继续修改对象图,继续产生SATB数据,所以仍然会有一小部分SATB数据,所以需要稍作停顿。书上写的清理阶段(Cleanup)就是过滤回收阶段。其实包括清洗阶段和回收阶段。这里我们只讨论清理阶段,不讨论回收。在这个阶段,NextBitmap和PrevBitmap会交换位置:所以,我们的图变成了下面这样:可以看到,NextBitmap和PrevBitmap交换位置,NextTAMS和PrevTAMS交换位置。在Region中,Bitmap白色部分对应的已用内存变为浅灰色。它只是标记,没有清理。需要注意的是:cleanup阶段不复制任何对象引用R的回答描述这个阶段:Inventoryandresetmarkstate。这个阶段有点像mark-sweep中的sweep阶段,但它不是清扫堆上的实际对象,而是统计标记位图中每个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.shtm2。https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html3.https://www.oracle.com/technetwork/java/javase/tech/g1-intro-jsp-135488.html4.https://www.infoq.com/articles/G1-One-Garbage-Collector-To-Rule-Them-All/5.https://hllvm-group.iteye.com/group/topic/443816.《深入理解Java虚拟机(第三版)》本文转载自微信公众号《何以科技》,可以使用以下两个二维码关注。转载本文请联系为什么科技公众号。
