HotSpot 卡表技术

来源极客时间 | 深入拆解 Java 虚拟机

卡表
为了解决在Minor GC中为了找出Minor GC中 所有的GC Roots 因为老年代中可能存在对新生代的引用,导致我们需要扫描整个老年代吗? HotSpot 给出的解决方案是一项叫做卡表(Card Table)的技术。

该技术将整个堆划分为一个个大 小为 512 字节的卡,并且维护一个卡表,用来存储每张卡的一个标识位。这个标识位代表对应的卡 是否可能存有指向新生代对象的引用。如果可能存在,那么我们就认为这张卡是脏的。
在进行 Minor GC 的时候,我们便可以不用扫描整个老年代,而是在卡表中寻找脏卡,并将脏卡中 的对象加入到 Minor GCGC Roots 里。当完成所有脏卡的扫描之后,Java 虚拟机便会将所有脏 卡的标识位清零。

由于 Minor GC 伴随着存活对象的复制,而复制需要更新指向该对象的引用。因此,在更新引用的 同时,我们又会设置引用所在的卡的标识位。这个时候,我们可以确保脏卡中必定包含指向新生代 对象的引用。

Minor GC 之前,我们并不能确保脏卡中包含指向新生代对象的引用。其原因和如何设置卡的标 识位有关。

首先,如果想要保证每个可能有指向新生代对象引用的卡都被标记为脏卡,那么 Java 虚拟机需要截 获每个引用型实例变量的写操作,并作出对应的写标识位操作。

这个操作在解释执行器中比较容易实现。但是在即时编译器生成的机器码中,则需要插入额外的逻 辑。这也就是所谓的写屏障(write barrier,注意不要和 volatile 字段的写屏障混淆)。
写屏障需要尽可能地保持简洁。这是因为我们并不希望在每条引用型实例变量的写指令后跟着一大 串注入的指令。

因此,写屏障并不会判断更新后的引用是否指向新生代中的对象,而是宁可错杀,不可放过,一律 当成可能指向新生代对象的引用。

这么一来,写屏障便可精简为下面的伪代码 [1]。这里右移 9 位相当于除以 512,Java 虚拟机便是通 过这种方式来从地址映射到卡表中的索引的。最终,这段代码会被编译成一条移位指令和一条存储 指令。

CARD_TABLE [this address >> 9] = DIRTY;

虽然写屏障不可避免地带来一些开销,但是它能够加大 Minor GC 的吞吐率( 应用运行时间 /(应用 运行时间 + 垃圾回收时间) )。总的来说还是值得的。不过,在高并发环境下,写屏障又带来了虚 共享(false sharing)问题 [2]。

在介绍对象内存布局中我曾提到虚共享问题,讲的是几个 volatile 字段出现在同一缓存行里造成的 虚共享。这里的虚共享则是卡表中不同卡的标识位之间的虚共享问题。

在 HotSpot 中,卡表是通过 byte 数组来实现的。对于一个 64 字节的缓存行来说,如果用它来加 载部分卡表,那么它将对应 64 张卡,也就是 32KB 的内存。

如果同时有两个 Java 线程,在这 32KB 内存中进行引用更新操作,那么也将造成存储卡表的同一部 分的缓存行的写回、无效化或者同步操作,因而间接影响程序性能。

为此,HotSpot 引入了一个新的参数 -XX:+UseCondCardMark,来尽量减少写卡表的操作。其伪代 码如下所示:

if (CARD_TABLE [this address >> 9] != DIRTY)  CARD_TABLE [this address >> 9] = DIRTY;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值