在新生代做GCRoots可达性扫描过程中,可能会碰到跨代引用的对象的问题,如下图所示:
图片
在新生代扫面的时候会扫描不到D对象,如果这个D对象不去老年代扫描是否有被引用,那么就会无法触达,如下如所示:
图片
但是D对象是有被引用的,如果直接回收D对象就会造成一些意想不到的问题,如果为了扫描D对象是否存在跨代引用而对老年代整体扫描一遍,就会带来整个垃圾回收的效率低下的问题。
为了解决跨代引用的问题,可以在新生代可以引入RememberSet(记录从非收集区到收集区的指针集合)的数据结构,称为记忆集,这样避免把整个老年代加入到GCRoots扫描范围中,如下所示:
图片
记忆集是一种概念,在hotspot使用名为“卡表”(Cardtable)的方式实现记忆集,它也是目前最常用的一种方式。卡表使用一个字节数组来实现:CARD_TABLE[],每个元素对应着其标识的内存区域一块特定大小的内存块,我们称之为“卡页”,如下图所示:
图片
将老年代按照一个卡页大小(512字节)分成n个区域,这个区域的地址信息都记录到卡表中。当某个区域中出现跨代引用的时候,我们就在卡表中记录信息,如下所示:
图片
老年代的F对象引用了年轻代的D对象,那么我们就在卡表中记录a卡页中有跨代引用(设置对应区域的值为1,如上a=1;卡页b没有跨代引用,设置b=0来表示此区域无跨代引用)。
当年轻代做GCRoots扫描的时候,我们去卡表中查询哪些区域存在跨代的对象,然后判断这个对象是否还继续存活,如下所示:
图片
一个卡页中可包含多个对象,只要有一个对象存在跨代引用,那么其对应在卡表中的元素标识就设置成1,表示该区域存在跨代引用,否则为0。GC时只要筛选本收集区的卡表中为1区域中的元素加入GCRoots里(如上图中a=1就被筛选出来做GCRoots)。
通过卡表的方式我们就避免了大规模的扫描老年代对象,假设老年代有上万的对象存活,年轻代之后几十个存活对象,通过卡表我们只需要扫面少部分的老年代对象,大大的提升垃圾收集的效率。
在hotspot使用写屏障维护卡表中数据状态,当老年代引用了新生代的对象时候,底层会维护将卡表中的对应的区域设置为1。