君子以自强不息。
卡表
GC 最早引入卡表的目的是为了对内存的引用关系做标记,从而根据引用关系快速遍历活跃对象。举个简单的例子,有两个分区,假设分区大小都为1MB,分别为 A 和 B。如果A中有一个对象 objA,B 中有一个对象 objB,且 objA.field=objB,那么这两个分区就有引用关系了,但是如果我们想找到分区 A,要如何引用分区 B?做法有两种:
- 遍历整个分区 A,一个字一个字的移动(为什么以字为单位?原因是 JVM 中对象会对齐,所以不需要按字节移动),然后查看内存里面的值到底是不是指向 B,这种方法效率太低,可以优化为一个对象一个对象地移动(这里涉及 JVM 如何识别对象,以及如何区分指针和立即数),但效率还是太低。
- 借助额外的数据结构描述这种引用关系,例如使用类似位图(bitmap)的方法,记录 A 和 B 的内存块之间的引用关系,用一个位来描述一个字,假设在32位机器上(一个字为32位),需要32KB(32KB×32=1M)的空间来描述一个分区。那么我们就可以在这个对象 ObjA 所在分区 A 里面添加一个额外的指针,这个指针指向另外一个分区B的位图,如果我们可以把对象 ObjA 和指针关系进行映射,那么当访问 ObjA 的时候,顺便访问这个额外的指针,从这个指针指向的位图就能找到被 ObjA 引用的分区 B 对应的内存块。通常我们只需要判定位图里面对应的位是否有1,有的话则认为发生了引用。
位图
以位为粒度的位图能准确描述每一个字的引用关系,但是一个位通常包含的信息太少,只能描述2个状态:引用还是未引用。实际应用中 JVM 在垃圾回收的时候需要更多的状态,如果增加至一个字节来描述状态,则位图需要256KB的空间,这个数字太大,开销占了25%。所以一个可能的做法位图不再描述一个字,而是一个区域,JVM 选择512字节为单位,即用一个字节描述512字节的引用关系。选择一个区域除了空间利用率的问题之外,实际上还有现实的意义。我们知道 Java 对象实际上不是一个字能描述的(有一个参数可以控制对象最小对齐的大小,默认是8字节,实际上 Java 在 JVM 中还有一些附加信息,所以对齐后最小的 Java 对象是16字节),很多 Java 对象可能是几十个字节或者几百个字节,所以用一个字节描述一个区域是有意义的。但是我没有找到512的来源,为什么512效果最好?没有相应的数据来支持这个数字,而且这个值不可以配置,不能修改,但是有理由相信512字节的区域是为了节约内存额外开销。按照这个值,1MB的内存只需要2KB的额外空间就能描述引用关系。这又带来另一个问题,就是512字节里面的内存可能被引用多次,所以这是一个粗略的关系描述,那么在使用的时候需要遍历这512字节。