Conservative GC (Part one)

目录

保守式GC

保守式GC(Conservative GC)指“不能识别指针和非指针的GC”

不明确的根

不明确的根(ambiguous roots),下面三类都可以作为根。事实上是不明确的根

  • 寄存器
  • 调用栈
  • 全局变量空间

以栈为例:在调用栈中有调用帧(call frame),调用帧里面装着函数内的局部变量和参数值。不过局变量中如果有c语言里面的int、double这样的数值,也就会有void*这样的指针。也就是说调用帧里既有数值(非指针),也有指针

GC不能识别指针和非指针,这类叫做不明确根。这样的GC算法称为保守式GC。

指针和非指针的区别

在不明确根这一条件下,GC不能准确识别指针和非指针。任意一个空间里可能是根也可能不是根。这样一来,GC在处理时就会出现大量指针识别错误因此保守式GC会检查不明确的根,以某种程度的精度来识别指针。

  • 保守式GC使用下列几种方式检查根
    • 是不是被正确对齐的值:32的cpu下指针的值为4的倍数,64位为8的倍数。如果是其他情况,就会被识别为非指针。
    • 是不是指着堆内 :当分配了GC专用堆时,对象就会被分到堆里。也就是说指向对象的指针一定指向这个堆。
    • 是不是指着对象的开头:调查不明确的根内的值是不是指着对象的开头。在标记清除那一节中,我们介绍了BiBOP.把对象按照固定大小对齐,核对对象的值是不是对象固定大小的倍数。

以上三种举措,根据内存布局和对象结构等检查项也会有所变化。

貌似指针的非指针

遇到非指针和堆内的对象的地址一样的情况。这个时候就无法识别这个值是非指针。这就是貌似指针的非指针(fasle pointer)。如图示:

保守式GC将这种“貌似指针的非指针”看出指针对象。我们把这种情况叫做“指针的错误识别”。

打个比方,在采用 GC 标记 - 清除算法的情况下,一找到貌似指针的非指针,程序就会将非指针指向的对象错误地识别为活动对象,对其进行标记。因为被错误识别的对象不会被废弃而会被保留,所以遵守了GC的原则—“不废弃活动对象”。像这样,在运行GC时采取的是一种保守的态度,即“把可疑的东西看作指针,稳妥处理”,所以我们称这种方法为“保 守式 GC”。

不明确数据结构

当基于不明确个根运行GC时,我们需要从对象的头部获取类型信息。比如说C中的结构体设置flag,通过flag就可以识别。
如果能从头中获得结构体信息,GC就能识别出对象域里的值是指针还是非指针。以C为例,所有的域里都包含类型信息,只要没有放入与类型不同含义的值,就有可能正确识别指针。

下列展示的结构体就会变成不明确的数据结构(ambiguous data structures)。

union{
    long n;
    void *ptr;
} ambiguous_data;

因为ambiguous_data是联合体,所以它可能包括指针ptr,或者非指针n。那么GC就无法准确识别出它是不是指针。当对象具有这样的数据结构时,GC不仅会错误识别不明确根,也会错误识别域里的值。

优点

  • 语言处理程序不依赖于GC

    易于编写语言处理程序,处理程序基本不用在意GC就可以编写代码。语言处理程序的实现者即使没有意实到GC的存在,程序也会自己回收垃圾。因此语言处理程序的实现要比准确式GC简单。

    缺点

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

    识别不明确的根和数据结构的值为“指针”或“非指针”时,我们需要付出一定的成本。

  • 错误识别指针会压迫堆

    保守式 GC 会把被引用的对象错误识别为活动对象。如果这个对象存在大量的子对象,那么它们一律都会被看成活动对象。因为程序把已经死了的非 活动对象看成了活动对象,所以垃圾对象会严重压迫堆。

  • 能够使用GC的算法有限

    在无法正确识别指针的环境中,我们基本上不能使用GC复制算法等移动对象的GC算法。我们想用不明确的根这么办的话,就可能把非指针重写了。此外,在对象内重写指针时,也有可能因为不明确的数据结构而重写了非指针。一旦重写了非指针,就会产生 意想不到的 BUG。

准确式GC

准确式GC(Exact GC)和保守式GC正好相反,它是能正确识别指针和非指针的GC。

正确的根

准确式GC和保守式GC不同,它是基于能准确识别指针和非指针的“正确的根(exact roots)”来执行GC的。

创建根的方法有很多种,不过这些方法的共同点就是需要“语言处理程序的帮助”,所以正确的根的创建方法是依赖于语言处理程序实现的。(可能要等好久我才能记录到那个地方。)

打标签

第一个方法是打标签(tag),目的是将不明确的根里的所有非指针都与指针区别开来。打标签的方法很多,最基本的低1位作为标签的方法。

在32位cpu的情况下,指针的值是4的倍数,低2位一定是0,我们就利用这个特性。在前面提到引用1位计数法,这次我们使用它来识别指针和非指针。

  • 打标签的具体方法:
    1. 将非指针int等,向左移1位(a << 1)
    2. 将低1位置位(a|1)

打标签的时候我们需要注意,比如在对数值打标签时,要注意不要让数据溢出。如果数据溢出,我们就得再变换一个更大的数据类型。

如果用这种方式打标签的话,所有数值都会是奇数。因此程序内进行计算时,必须取消标签位在计算。

基本上打标签和去标签都是语言处理程序完成的,这就是之前说的需要语言处理程序的帮助。

为不明确的根里的所有非指针打标签后,GC就能正确的识别指针和非指针了即正确识别指针。

不把寄存器和栈等当做根

不把寄存器和栈等不明确因素当做根,而是在处理程序里创建根。

具体来说就是,创建一个正确的根来管理,这个正确的根在处理程序里只集合了mutator可能到达的指针,然后以他为基础进行GC。

优点

首先,准确式GC完全没有保守式GC固有的问题--错误识别指针。此外它还可以实现GC复制算法等移动对象的算法。准确式GC可以正确的识别指针,所以即使移动对象,重写根这个对象也是正确的。

缺点

在创建语言处理程序时,必须照顾到GC。这会给实现算法带来一定的负担。在处理上也比较麻烦。

此外,创建正确的根就必须付出一定代价,比如说打标签。这实际关系到语言的处理速度。

间接引用

保守式 GC 有个缺点,就是“不能使用 GC 复制算法等移动对 象的算法”。解决这个问题的方法之一就是“间接引用”。

经由句柄引用对象

在保守式GC中有一个问题,无法使用垃圾回收算法。原因就是因为就不能明确的根,重写的对象有可能是非指针。

这个问题可以通过句柄(handle)来间接的处理对象。

从下图可以看出,根和对象之间有句柄。每个对象都有一个句柄,他们分别持有指向这些对象的指针。并且局部变量和全局变量这些不明确的对象的根里没有只想对象的指针,只装着指向句柄的指针。也就是说,有mutator操作对象时,要通过经由句柄的间接引用来执行处理。

采用句柄之后,就算是移动对象,也只需要修改句柄里的指针就行了。也就是说通过句柄间接访问对象。

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

使用间接引用的GC算法不是准确式GC。因为间接引用是以不明确的根为基础运行的GC,所以还不能正确识别指针和非指针。也就是说,还是会发生错误识别的情况。所以他也是保守式GC。

优缺点

优点:使用间接引用有可能实现GC复制算法,所以GC复制算法的优点就是它的优点。例如消除碎片化。
缺点:对象都经由句柄简介引用,所以会拉低访问内存对象的速度,这关系到整个语言处理程序的速度。

猜你喜欢

转载自www.cnblogs.com/Leon-The-Professional/p/10018386.html
GC
one