关于后端:JVM是如何解决跨代引用问题的

1次阅读

共计 2421 个字符,预计需要花费 7 分钟才能阅读完成。

本文已收录至 Github,举荐浏览 👉 Java 随想录

微信公众号:Java 随想录

CSDN:码农 BookSea

不晓得本人的无知,乃是双倍的无知。——柏拉图

跨代援用问题

跨代援用是指新生代中存在对老年代对象的援用,或者老年代中存在对新生代的援用。

如果要当初进行一次只局限于新生代区域内的收集(Minor GC),但新生代中的对象是齐全有可能被老年代所援用的,为了找出该区域中的存活对象,不得不在固定的 GC Roots 之外,再额定遍历整个老年代中所有对象来确保可达性剖析后果的正确性,反过来也是一样。无疑会为内存回收带来很大的性能累赘。

别慌,JVM 的设计者曾经思考了这个场景,并想到了解决办法,那就是应用一种叫做:记忆集(Remembered Set)的数据结构。

记忆集

记忆集位于新生代中。用以防止把整个老年代加进 GC Roots 扫描范畴

记忆集的作用和咱们之前讲的 OopMap 很类似,保护了相似一种映射表的关系,防止了全局扫描,实质是用空间换工夫

记忆集是一种用于记录从非收集区域指向收集区域的指针汇合的形象数据结构。留神这里的说辞:形象。意思就是说记忆集是一种逻辑上的概念,并没有规定具体的实现,相似办法区。下文咱们会说到卡表,能够把记忆集和卡表的关系了解为 Map 跟 HashMap

卡表

卡表能够了解为是记忆集的具体实现。英文叫:Card Table

垃圾收集器只须要通过记忆集判断出某一块非收集区域是否存在有指向了收集区域的指针就能够了,并不需要理解这些跨代指针的全副细节。那设计者在实现记忆集的时候,便能够抉择更为粗暴的记录粒度来节俭记忆集的存储和保护老本,上面列举了一些可供选择(当然也能够抉择这个范畴以外的)的记录精度:

其中,第三种“卡精度”所指的就是“卡表”的形式去实现记忆集,这也是目前最罕用的一种记忆集实现模式,HotSpot 采纳的就是卡表

在 HotSpot 虚拟机外面,卡表采纳的是字节数组的模式。以下这行代码是 HotSpot 默认的卡表标记逻辑:

CARD_TABLE [this address >> 9] = 0;

字节数组 CARD_TABLE 的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个内存块被称作“卡页”(Card Page)。一般来说,卡页大小都是以 2 的 N 次幂的字节数,通过下面代码能够看出 HotSpot 中应用的卡页是 2 的 9 次幂,即 512 字节。那如果卡表标识内存区域的起始地址是 0x0000 的话,数组 CARD_TABLE 的第 0、1、2 号元素,别离对应了地址范畴为 0x0000~0x01FF、0x0200~0x03FF、0x0400~0x05FF 的卡页内存块,如图所示:

一个卡页的内存中通常蕴含不止一个对象,只有卡页内有一个(或更多)对象的字段存在着跨代指针,那就将对应卡表的数组元素的值标识为 1,称为这个元素变脏(Dirty),没有则标识为 0。在垃圾收集产生时,只有筛选出卡表中变脏的元素,就能轻易得出哪些卡页内存块中蕴含跨代指针,把它们退出 GC Roots 中一并扫描。

简略来说,就是卡页的字节数组只有 0 和 1 两种状态,1 示意哪些内存区域存在跨代指针,那么只有把 1 的退出 GC Roots 中一并扫描,就能晓得哪些进行跨代援用了,这样就不必挨个去扫描了

OK,咱们还剩下一个问题,这个问题 OopMap 也遇到过。卡表元素如何保护?何时变脏、谁来把它们变脏等。

HotSpot 解决的方法是应用写屏障

写屏障

先来解决何时变脏的问题,这个问题很简略,即 其余分代区域中对象援用了本区域对象时,其对应的卡表元素就应该变脏,变脏工夫点原则上应该产生在援用类型字段赋值的那一刻

但问题是如何变脏,即如何在对象赋值的那一刻去更新保护卡表,在 HotSpot 虚拟机里是通过 写屏障(Write Barrier)解决的。

留神:这里提到的 写屏障 和 volatile 的写屏障不是一回事。

写屏障能够看作在虚拟机层面对“援用类型字段赋值”这个动作的 AOP 切面,在援用对象赋值时会产生一个环形(Around)告诉。用过 Spring 的弟兄们对 AOP 必定不生疏。

在赋值前的局部的写屏障叫作 写前屏障(Pre-Write Barrier),在赋值后的则叫作 写后屏障(Post-Write Barrier)。HotSpot 虚拟机的许多收集器中都有应用到写屏障,但直至 G1 收集器呈现之前,其余收集器都只用到了写后屏障。

利用写屏障后,虚拟机就会为所有赋值操作生成相应的指令,一旦收集器在写屏障中减少了更新卡表操作,无论更新的是不是老年代对新生代对象的援用,每次只有对援用进行更新,就会产生额定的开销,不过这个开销与 Minor GC 时扫描整个老年代的代价相比还是低得多的。

写屏障的伪共享问题

卡表在高并发场景下还面临着 “伪共享”(False Sharing) 问题。伪共享是解决并发底层细节时一种常常须要思考的问题,号称并发的 隐形杀手,古代中央处理器的缓存零碎中是以缓存行(Cache Line)为单位存储的,当多线程批改相互独立的变量时,如果这些变量恰好共享同一个缓存行,就会彼此影响(写回、有效化或者同步)而导致性能升高,这就是伪共享问题。

为了防止伪共享问题,一种简略的解决方案是不采纳无条件的写屏障,而是先查看卡表标记,只有当该卡表元素未被标记过期才将其标记为变脏,行将卡表更新的逻辑变为以下代码所示:

相当于说就是多了一个 if 判断条件。

if (CARD_TABLE [this address >> 9] != 0)
CARD_TABLE [this address >> 9] = 0;

在 JDK 7 之后,HotSpot 虚拟机减少了一个新的参数-XX:+UseCondCardMark(默认是敞开的),用来决定是否开启卡表更新的条件判断。开启会减少一次额定判断的开销,但可能防止伪共享问题,两者各有性能损耗,是否关上要依据利用理论运行状况来进行测试衡量。


如果本篇博客有任何谬误和倡议,欢送给我留言斧正。文章继续更新,能够关注公众号第一工夫浏览。

正文完
 0