保守式GC

保守式GC指的是“不能识别指针和非指针的GC”

不明确的根

在之前提到过,下面这些都属于根(root)

  • 寄存器
  • 栈变量
  • 全局变量

事实上这些都是不明确的根,即不知道它们是指针还是非指针数据

以调用栈为例,调用栈里装着栈帧,栈帧里有局部变量和参数的值,这些值即可能是int、double这样的非指针,也可能是void*这样的指针,这些值在GC看来就是一堆位的排列,因此GC无法分辨指针和非指针

指针和非指针的识别

在不明确的根中,GC无法识别指针和非指针,也就是说根中所有所有值都可能是指针,然而这样一来,在GC时就会大量出现指针和被错误识别为指针的指针

因此保守式GC会检查根,按照下述规则识别指针:

  1. 是不是被正确对齐的值?(32位CPU下为4的倍数,64位下是8的倍数)
  2. 是不是指着堆内?
  3. 是不是指着对象的开头?

貌似指针的非指针

偶尔会有非指针和堆里的对象的地址一样,此时GC就无法识别出这个值是非指针,这就是“貌似指针的非指针”

在面对这种被错误识别为指针的非指针时,GC就会将其“指向”的对象标记为活动对象,而不进行回收

像这样,在面对可疑对象时采取保守态度对待,宁可放过不可杀错,所以称其为“保守式GC”

不明确的数据结构

当基于不明确的根运行GC时,需要从对象头部获取对象的类型信息

如果能从对象头的标志中获得类型信息,GC就能识别对象域的值是指针还是非指针

不过数据结构要是像下面这样,就会变成不明确的数据结构

1
2
3
4
union{
long n;
void *ptr;
} ambiguous_data;

ambiguous_data是个union,可能包括指针ptr,或非指针n,如果n是一个“貌似指针的非指针”,那么GC就无法识别出它是否确实是个指针

当对象具有这样的数据结构时,GC不仅会错误识别不明确的根,也会错误识别于里的值

优点

保守式GC的优点在于容易编写语言处理程序,处理程序基本不用在意GC就可以编写代码,程序员即使没有意识到GC的存在,程序自己也会回收垃圾,因此语言处理程序的实现比精准式GC简单

缺点

识别指针和非指针需要付出成本

识别不明确的根和数据结构的值为指针或非指针时,需要付出额外的成本,而这一成本在精准式GC里是不存在的

错误识别指针会压迫堆

在存在貌似指针的非指针时,保守式GC会将其指向的对象视为活动对象,如果此对象还存在大量子对象,那么会被一并当做活动对象而不进行回收,这么做会导致垃圾对象严重压迫堆

能够使用的GC算法有限

在不能精准识别指针的环境中,是无法使用GC复制法等需要移动对象的GC算法的

因为GC复制法在复制对象时,会将根中指针对象的值重写为移动后的新对象的地址,这样可能到把非指针对象也重写了

此外在对象内重写指针时,也有可能因为不明确的数据结构而重写了非指针,最终导致意想不到的Bug发生

精准式GC

精准式GC和保守式GC正好相反,它能识别出指针和非指针

明确的根

精准式GC是基于能准确识别指针和非指针的“明确的根”运行的

创建明确的根的方法很多,但都需要语言处理程序的支持

打标签

第一个方法就是打标签(tag),目的是将不明确的根中的指针和非指针区分开来,此处以最基本的低1位法为例

在32位CPU下,指针是4的倍数,低2位一定是0,可以利用这个特性来识别指针和非指针

打标签的具体方法如下

  1. 将非指针(int等)左移1位(a << 1)
  2. 将低1位或1(a | 1)

需要注意,在对数值打标签时,要注意不要让数据溢出,左移1位如果数据溢出了,就得再变换一个大的数据类型

用这种方法打标签的话,处理程序的数值就会都是奇数,在进行计算时必须取消标签再运算

不把寄存器和栈当做根

还有一种方法就是不把寄存器和栈等不明确的根当做根来用,而在处理程序里创建根

具体思路就是创建一个明确的根来管理,以其为基础执行GC

优点

精准式GC完全没有保守式GC固有的问题——错误识别指针,它不会认为已经死了的对象还活着,GC后堆里只会留下活动对象

此外还支持实现GC复制法等需要移动对象的算法,因为它能明确分辨出非指针,即使移动对象,重写根的值,这个对象也不可能是非指针

缺点

当创建精准式GC时,必须得到语言处理程序的支持,因此实现上比保守式GC麻烦

此外,要创建明确的根必须付出一定的代价,比如打标签后,就必须每次计算都压取消标签,计算完后再重新设置

间接引用

经由句柄引用对象

保守式GC无法移动对象是以为可能重写了非指针的值

解决这个问题的办法就是用句柄(handler)来间接引用对象

从下图中可以看到,根和堆对象之间隔了层句柄,每个堆对象对应一个句柄,句柄持有指向堆对象的指针

不明确的根里没有指向对象的指针,只有指向句柄的指针

只要采用了间接引用,那么即使移动了对象,也不用该写根中的值,只需要改写句柄的值即可

而且在对象内没有经由句柄指向别的对象,只有从根引用对象时才会经由句柄

此外,使用了间接引用的GC仍然是保守式GC,无法分辨指针和非指针

优点

使用间接引用的情况下有可能实现GC复制法、标记-压缩法等

缺点

因为所有对象都经由句柄间接引用,所以会拉低访问对象内数据的速度