HotSpot垃圾算法实现之记忆集与卡表和写屏障
问题一:对象跨代(区域)引用,GC Roots扫描范围如何界定?
当部分区域进行垃圾收集时,如果非收集区域的对象跨区引用了收集区域的对象(收集区域的对象A可能被非收集区域的对象静态字段B引用,这样对象A应加入GC Roots集合,防止被错误清理),虽然可以笼统的将其他所有区域都加入到GC Roots的扫描范围,可如此势必会有额外的性能消耗。 因此,HotSpot加入了记忆集(Remembered Set)来避免扫描范围过大带来的性能消耗。
问题二:记忆集是什么?
记忆集是一种用于记录非收集区域指向收集区域的指针集合的抽象数据结构。 如果不考虑成本和效率的话,最简单的实现可以用非收集区域中所有含跨代(区域)引用的对象数组来实现这个数据结构。
问题三:记忆集实现精度?
有3种实现精度,分别如下:
- 字长精度:每个记录精确到一个机器字长(JVM地址),表示该字包含跨代指针。
- 对象精度:每个记录精确到一个对象,表示该对象里有字段包含有跨代指针。
- 卡精度:每个记录精确到一块内存区域,表示该区域内有对象包含有跨代指针。
问题四:HotSpot虚拟机记忆集的实现时什么?
HotSpot虚拟机用一个字节数组来实现卡精度,也叫做卡表的方式实现记忆集。 卡表(card table)的每一个元素对应内存区域中一块特定的内存块,叫做卡页(card page)。 一般来说卡页大小都是2的n次幂,HotSpot中卡页大小为2的9次幂,即512字节。地址右移9位,得到的是卡表的索引序号。一个卡页的内存中通用有多个对象,但是只有卡页中有一个对象的字段存在着跨代指针,则将该卡页对应的卡表索引元素值标识为1(称为变脏),没有则标识为0。在垃圾收集发送时,只要将卡表中标识为1的对应的卡页内存块加入到GC Roots中一并扫描。
问题五:卡片元素何时变脏?
有其他分代区域中的对象引用了本区域对象时,在引用字段类型赋值的时刻将其(引用字段)对应的卡表元素变脏。 HotSpot虚拟机通过写屏障Write Barrier技术来维护卡表状态。(这里的写屏障和内存屏障无关)
问题六:写屏障是什么?
写屏障可以看着时虚拟机层面对“引用类型指端赋值”这个动作的AOP切面。在引用对象赋值是会产生一个环形(Around)通知,供程序执行额外动作。赋值前的写屏障叫写前屏障,赋值后的写屏障叫写后屏障。
问题七:卡表更新伪共享问题如何解决?
HotSpot虚拟机应用写屏障后,就会为所有赋值操作生成相应的指令,一旦收集器在写屏障中新增了更新卡表操作,就会执行卡表更新操作。而处理器缓存行多为64字节,且一个卡表元素占一个字节,所以64个卡表元素共用同一个缓存行。这64个卡表元素对应的卡页大小为32KB(64x512字节=64x2^9),也就是说如果不同线程更新的对象正好在这32KB内存区域内,就会导致更新卡表时正好写入同一个缓存行而影响性能。
为了避免伪共享问题,JDK7后HotSpot增加了-XX:+UseCondCardMark
参数来开启卡表更新时判断卡表标记为过时,未过时才将其标记记为变脏。
参考资料
- 周志明 * 《深入理解Java虚拟机》