JVM学习日记⭐️HotSpot算法细节实现(中)⭐️🔉引

“这是我参与8月更文挑战的第18天,活动详情查看:8月更文挑战

🔉引言

昨天说了HotSpot算法细节的实现,算法算法,效率第一,才能保证虚拟机的高效运行,但没说完,今天补全。

📇记忆集与卡表

在讲记忆集之前,不知道大家还记不记得,我在讲对象的跨代引用时,提到过记忆集,不记得的可以用过【传送门】,进去看一看。记忆集(Remembered Set)可以避免我们把整个老年代的对象都放入GCRoots里进行扫描,实际上不光是我们的跨代之间会遇到这样的问题,我们的所有涉及部分行为的垃圾收集器都会遇到此类问题,因此进一步理解和学习记忆集的工作原理和方式是必要的。

记忆集的本质就是一种数据结构,什么样的数据结构呢?记录从非收集区域到收集区域的指针的指针集合,最简单的就是使用非收集区域中跨代引用的对象集合来表示。

这种记录方式成本高昂,事实上,收集器只需要通过记忆集来判断某一块非收集的区域是否存在收集区域的引用指针就可以了,并不需要理解跨代引用的实际细节,在实现上我们可以选择更为粗犷的粒度来节省记忆集的存储和维护成本,以下是记录精度的参考:

  • 字长精度:每个记录精度精确到一个字长,该字包含跨代指针
  • 对象精度:每个记录精确到一个对象。对象里有字段记录跨代指针
  • 卡精度:每个记录精确到一块内存区域,该区域的对象含有跨代指针

🎴卡表

以卡精度的粒度来记录的方式我们也称为卡表card table),卡表与前面提到的记忆集不同,记忆集是一种抽象的数据结构,只定义了记忆集的行为意图,并没有定义具体实现,而卡表定义了记忆集的具体实现,它定义了记忆集的记录精度和堆内存的映射关系等。

那卡表是怎么玩的呢?提到表我们是不是会联想到数据库?昂,里面存储着女神的信息,好了,赶紧回来,字节数组Card_Table的每一个元素都对应着其标识的内存区域的一个固定大小的内存块,这样的内存块也被称为“卡页”(Card Page),卡页的大小都是以2N次幂大小的字节数,我们的HotSpot的卡页的大小29次幂即512个字节,那如果起始地址是0就对应0,然后1对应512(10进制)2对应102410进制)。图如下:

image.png

当卡页有一个(或多个)对象的字段里面存储着跨代指针,那么将对应卡表的数组元素值标识为1,这时称这个元素变脏(dirty),那当垃圾收集发生时,我们只需要把这些变脏的元素找到,逼它说出对应内存的跨代引用指针,一一送给GCRoots检验就欧克了。

🎴卡表维护

接下来说说卡表如何维护的问题,谁来给它变脏?何时变脏呢?何时变脏比较简单,只要有其他分代区域的对象引用了该区域的对象,那其对应的卡表元素就应该变脏,那问题是如何变脏呢?如何在对象赋值的那一刻就更新卡表呢?

这时候应该严刑拷打虚拟机了,哈哈,不玩了,HotSpot是通过写屏障来维护卡表的(Write Barrier),怎么玩的呢?就是对象在进行引用赋值之前,先切一刀,塞一个写前屏障进去,赋值后,在塞一个写后屏障,在写后屏障进行卡表的更新,这样赋值前后都在写屏障的范围内。

应用写屏障后,虚拟机为所有赋值操作生成对应的指令,只要更新卡表操作,就会产生额外的开销,此外还会遇到“伪共享”的问题,因为现代中央处理器的缓存中是以缓存行(Cache Line)为单位存储的,那高并发场景下,当多线程修改变量时,如果这些变量都位于同一个缓存行,就会在更新卡表时写入同一个缓存导致性能降低。

JDK7以后,我们可以通过参数-XX:+UseCondCardMark来开启卡表更新条件的判断,只有卡表元素未被标记时,才将其标记为变脏,这样避免伪共享的问题。

📝题外话

人虽然可以为所欲为,但却不能得偿所愿-叔本华。

这句话就让我深受启发。每当自己或他人经历种种磨难时,这句话总能给我带来慰藉,成为无穷无尽的宽容的源泉。幸运的是,这种认识不仅能缓解那种让人感到无能为力的责任感,也能防止我们过于严苛地对待自己和他人。