HotSpot中的常用知识点

记录一些Java虚拟机的一些常见知识点,是根据周志明老师的《深入理解Java虚拟机》3.4节的学习总结

1、OopMap

​ Java在垃圾回收阶段查看对象是否属于垃圾,需要借助可达性分析算法查找GC Roots到某个对象是否存在引用链。而检索的阶段我们是不希望查看的对象还处在可以不断变化的过程。我找着找着,刚确认完你还活着,转头你就被弄死了;或者刚确认完你在这儿,一会儿你又跑那里去了😭😭。也就是说,我们不希望这个阶段是多线程在执行的环境在执行。

​ 所以不管是CMS还是G1,都会有STW的步骤(stop the world,用户线程暂停),这肯定是我们不太希望的。

​ 可以想象一下,当我们开发一个网站,或者做个小游戏,用户每隔一段时间就要等待个几毫秒或是几秒是一件多么让人绝望的事儿,我们肯定是希望STW的时间尽可能短的。

​ 但是如果某个引用链很长,并且可能还存在跨分带区域的引用的时候,这样一路顺藤摸瓜找下来时间肯定不会太短的,我们希望可以快速找到引用的对象,这里就用到了OopMap数据结构。

​ OopMap可以帮助我们直接得到哪里保存着对象的引用,一旦类加载动作完成后,HotSpot会把对象内什么偏移量上是什么类型的数据计算出来,在即时编译的过程中,也会在特定位置上记录下栈、寄存器哪些位置是引用

​ 可以这样理解,我们网上买了一件衣服,想知道生产厂家到底是哪里,可以这样做:

  • 找到淘宝卖家,然后找到卖家的进货地,在从进货地找到批发商,联系批发商找到他的货源地,再根据与货源地的沟通找到生产厂家,可能仅仅半个月的时间就够了
  • 或者直接查看衣服后面的标牌,看看生产厂家到底在哪里,可能要长达2s的时间...😮

​ OopMap就充当了这么一个标牌的角色,它在当前对象保存了一些附加信息,可以直接找到引用对象。

2、Safepoint安全点

​ 根据上面我们可以知道,可以通过给对象保存一些附加信息,实现快速找到其引用的目的。但是又有一个问题,OopMap相当于额外记录了不少内容,如果每个对象都使用了OopMap那内存肯定大量消耗。这也不是我们希望的。

​ 可以采用这样的方式,不需要在每个对象都写入OopMap额外信息,而是在特定位置记录一下,这些位置的距离既不能太长,太长就很难快速达到,也不能太短了,太短耗费内存空间,所以一般选择那种可以让程序长时间执行特征的位置,比如:方法调用、循环跳转、异常跳转等位置。这些位置就是Safepoint安全点,也就是我们垃圾回收器步骤常见的那个安全点。

img

​ 有了安全点,用户线程就不是执行到任意位置都能停下来了,而是要走到Safepoint才能停下来。

​ 那万一在将要STW的时候,用户线程没跑到Safepoint怎么办?

​ 军训的时候,教官一声号令:集合!!!大家都集合好了,有几个人墨迹没赶上,教官抓住以后骂一顿让他们归队;或者教官可以这样,下令:给你们3分钟赶紧下来集合,这回大家都听到号令,很快就集合完毕了~

​ 垃圾回收器也有两种选择:抢先式中断主动式中断抢先式中断是当垃圾收集发生时,所有用户线程都被中断,查看是不是有没到安全点的,有的话就放行让它到最近的安全点上,但这种方式几乎不用了;主动式中断是当要中断线程时,先设置一个标志位,各个线程轮询查看这个标志,发现标志为true的时候,说明要垃圾回收了,各个线程乖乖的跑到最近的安全点上挂起。

3、SafeRegion安全区域

​ 现在假如一个线程处于Sleep或Block状态,它没办法在要垃圾回收的时候到达安全点怎么办呢?这里就可以设置安全区域了,通过设置一段代码都是安全的,它的引用关系不会随着标记和垃圾回收而发生改变,这样的区域就是SafeRegion。

4、记忆集和卡表

​ 如果存在跨代引用问题的话,比如某个新生代对象是由老年代对象引用的,那么是需要整个老年代都扫描到吗?没有一种方式可以标记一下老年代哪些位置是占用状态的,然后只找这些位置吗?答案是有的,我们常采用在新生代中建立记忆集(Remembered Set)避免扫描整个老年代区域查看哪些是GC Roots。

​ 而实现Remembered Set最常用的就是按照卡精度来实现的,这就是卡表(Card Table)。卡表中包含一块字节数组CARD_TABLE,其中每一个元素都对应着一个内存块,也被称为卡页Card Page,一个卡页不只一个对象,如果某个对象有引用,那在这个卡表中的这个位置标为1,代表这个部分变脏了,当垃圾回收时,只会扫描这些变脏的区域,大大提升检索效率。

5、写屏障

​ HotSpot通过写屏障来维护卡表,也就是说在把引用赋值写入内存之前,先要把存在跨代引用的内存块位置变脏。

6、并发情况下的标记

​ 一般初始标记我们会STW,这个时间段很短,但是不能完全满足要求,这个时间只是简单的检索到直接和GC Roots相互关联的对象。这个时间虽然短但是没办法帮我们访问所有对象,如果访问所有对象还是STW,肯定要花费很长时间,这里需要使用并发标记节省时间。

​ 但是并发标记阶段,多个线程标记存活对象可能会产生线程安全问题:

  • A对象,在①线程检测标记了未存活状态,在②线程又检测一遍,错误的记录成存活,导致其此次没有清理,变为浮动垃圾
  • A对象,在①线程标记为存活状态,在②线程检测下,修改为死亡状态,结果错杀了好人

​ 如果仅仅是产生浮动垃圾也还好,大不了时间长一点,等待下次垃圾回收就好了,如果诬陷了本应存活的对象,害的对象被清除了,那可就完蛋了,可能会导致程序崩溃不说,有的甚至遗臭万年😅😅

img

​ 一般在并发标记阶段,我们采用三色标记法进行标记,它与普通的标记还不一样,对象可以有3种状态:

  • 黑色:对象以前被垃圾回收器访问过,而且所有的引用都引用着

  • 白色:新加的对象,还没有引用过,或者不被引用了

  • 灰色:对象被引用,且也被垃圾回收器扫描过了,但是有新的对象被它引用

    那么怎么解决上面的多线程情况下错误标记对象,让对象消失的情况呢?

​ 大牛们曾经研究并证明过,有两个条件必须同时满足才会让对象消失,也就是把以前存活的对象(黑色)错误标记为未存活(白色):

  • 赋值器插入了一条或多条从黑色对象指向白色对象的新引用
  • 赋值器删除了全部从灰色对象到该白色对象的直接或间接的引用

​ 通俗点说,老教父维托柯里昂跟五大家族火并后和谈的时候,发誓自己绝对不会做那个挑起战争的人(老子不会断掉和你们的引用关系),然后就把迈克扶植成新教父(添加了一条新的引用),迈克一举干掉了五大家族成功复仇(删除该对象的的引用)。怎么样这个对象才能被干掉呢?加一条引用,然后清除它们的引用关系。

img

​ 也就是说,只要破坏这两个条件中的任何一个都能解决上面的对象消失问题。

​ 方式一叫做增量更新(Incremental Update):当黑色对象中插入了指向白色对象的引用关系,那么就把这个新插进来的引用记录下来,等待并发扫描结束之后,再以这个对象为根扫描一次。也就是说,黑色对象中一旦新插入了指向白色对象的引用之后,它就会变成灰色。

​ 方法二叫做原始快照(Snapshot At The Begginning,也就是我们常说的SATB):当灰色对象要删除指向白色对象的引用关系时,就把这个要删除的引用记录下来,并发扫描结束后,将这些记录过的引用关系的灰色对象为根重新扫描一次。

​ 不同的垃圾回收器采用的是不同的方式避免对象消失问题,比如CMS垃圾收集器采用的就是增量更新的方式,G1垃圾收集器采用的就是SATB的方式。