上面的图片是我上周末在家拍的。以后的文章里面我的第一张配图都用自己随手拍下的照片吧。分享生活,分享技术,哈哈。
阳台上的花开了,成都的春天快来了,疫情也应该快要过去了吧。
最近在看《霍乱时期的爱情》,不知道为什么和《大话西游》联系了起来,所以你可以看到玻璃上的倒影,是我在看《大话西游》。
谁都曾经有过大闹天宫的梦想,爱上层楼的忧愁,但是早晚有一天,你也会像他转身之后一样,走在路上,像一条狗。
好了,说回文章
让你看看“浮动垃圾”
上《面试官:你说你熟悉jvm?那你讲一下并发的可达性分析》这篇文章主要聊了 jvm 的可达性分析算法。
借助“三色标记”大法分析了垃圾回收线程扫描的过程中,用户线程同时执行修改引用关系的操作时,可能会出现的“对象消失”问题,以及其对应的两种解决方案
增量更新和原始快照。
在文章中我写道:对象关系图的变化会导致出现两种情况一是“浮动垃圾”,二是“对象消失”。大概率的情况下面试官更加关心第二种情况,因为第二种情况会给程序带来异常。接下来我就做动图分析了“对象消失”的情况
但是我是万万没想到呀,读者更关心的是“浮动垃圾”。有的读者就来问我,浮动垃圾是怎么产生的,你倒是给个图啊。
像我这样的又暖又有料的硬核原创作者,你说你要,那我肯定是要给你的。
下面就给你补上“浮动垃圾”的动图:
当并发标记完成后,对象图就变成了下面这个样子:
你看出来了吧。对象7,8,4,11,10都是浮动垃圾。因为他们被标记成了黑色,所以逃过了本次垃圾回收。
什么?你问我为什么黑色就不回收了?你个假粉丝,建议你先去读一读上周的文章。
G1垃圾回收时新对象怎么处理?
有的读者就提出了另外的很有探讨性的问题:
why哥你好,你《面试官:你说你熟悉jvm?那你讲一下并发的可达性分析》这篇文章主要解决了在并发标记阶段,GC线程和用户线程并发执行时,用户线程修改了对象引用关系,导致“对象消失”的问题。G1是采用原始快照加写前屏障的方式解决这个问题的。
但是我还有另外的一个问题:用户线程执行时不仅修改了对象引用关系,还新分配了新对象,我觉得这个情况是非常常见的,G1是如何找到并处理这些对象的呢?
换句话说,就是文章标题啦:G1收集器是怎么知道这些对象是什么时候应该进行垃圾标记的?
这是一个好问题,一看就是用心读了文章并带有自己的思考。很不错。
这位读者的问题属于第一个问题的连环炮,让我突然有了一种掉进了面试官布好的天罗地网里面的感觉。
面试官先故意漏出破绽,让你聊“对象消失”、“三色标记”、“增量更新”。然后等你得意洋洋的时候,突然抛出第二个问题:刚刚对象消失的问题回答的不错,那如果并发标记的时候用户线程分配了新对象,G1是怎么处理的呢?
说实话,我觉得只要你简历上没有写精通jvm,面试一般问到这种程度的我觉得是真的到了探讨的地步了。答的上来加分,答不上来也不扣分。
遥想2016年,我刚毕业,只身闯北京的时候,一连面试了9家公司,没有一家公司聊到 jvm (当然我当时面的是初级开发)。现在不一样了,不知道什么时候 jvm 从进阶面试题,变成了初级面试题。面试阶段如果没有问 jvm ,就感觉不是一次完整的面试。
我觉得就这几年面试题的变化,其实也就是反映了一个现象:想入行的人越来越多,导致入行的门槛越来越高。
不是jvm的地位变了,而是门槛越来越高了。
好了,瞎逼逼完了,接下来我们聊聊G1。
初识Garbage First(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的默认个数。
事了拂衣去,深藏功与名。