标记(mark)阶段和清除(sweep)阶段组成
标记阶段标记出根节点的所有可达的和已分配的后继
而后面的清除阶段释放每个未被标记的已分配块

隐式空闲链表的实现 块头部中空闲的低位中的一位通常用来表示这个块是否被标记了

我们对 Mark&Sweep 的描述将假设使用下列函数,其中 ptr 定义为 typedef void* ptr:

  • ptr isPtr (ptr p)。如果 p 指向一个已分配块中的某个字,那么就返回一个指向这个块的起始位置的指针 b。否则返回 NULL。
  • int blockMarked(ptr b)。如果块 b 是已标记的,那么就返回 true。
  • int blockAllocated (ptr b)。如果块 b 是已分配的,那么就返回 true。
  • void markBlock (ptr b)。标记块 b。
  • int length (b)。返回块 b 的以字为单位的长度(不包括头部)。
  • void unmarkBlock (ptr b)。将块 b 的状态由已标记的改为未标记的。
  • ptr nextBlock (ptr b)。返回堆中块 b 的后继。
    标记阶段为每个根节点调用一次 mark 函数。如果 p 不指向一个已分配并且未标记的堆块,mark 函数就立即返回。否则,它就标记这个块,并对块中的每个字递归地调用它自己。每次对 mark 函数的调用都标记某个根节点的所有未标记并且可达的后继节点。在标记阶段的末尾,任何未标记的已分配块都被认定为是不可达的,是垃圾,可以在清除阶段回收。
void mark(ptr p) {
    if ((b = isPtr(p)) == NULL)
        return;
    if (blockMarked(b))
        return;
    markBlock(b);
    len = length(b);
    for (i = 0; i < len; i++)
        mark(b[i]);
    return;
}

清除阶段是对 sweep 函数的一次调用。sweep 函数在堆中每个块上反复循环,释放它所遇到的所有未标记的已分配块(也就是垃圾)。

void sweep(ptr b, ptr end) {
    while (b < end) {
        if (blockMarked(b))
            unmarkBlock(b);
        else if (blockAllocated(b))
            free(b);
        b = nextBlock(b);
    }
    return;
}

下面展示了一个小堆的标记清除算法的图形化解释。块边界用粗线条表示。每个方块对应于内存中的一个字。每个块有一个字的头部,要么是已标记的,要么是未标记的

标记-清除算法垃圾收集器

Link to original

Note

注意看,沿着根节点,我们顺着指针能走到底的被标记的快头部,就是所谓的可达节点,就是申请了,同时被好好使用着的部分,而不可达的淡蓝色区域就是垃圾
初始情况下,图中的堆由六个已分配块组成,其中每个块都是未分配的。第 3 块包含一个指向第 1 块的指针。第 4 块包含指向第 3 块和第 6 块的指针。根指向第 4 块。在标记阶段之后,第 1 块、第 3 块、第 4 块和第 6 块被做了标记,因为它们是从根节点可达的。第 2 块和第 5 块是未标记的,因为它们是不可达的。在清除阶段之后,这两个不可达块被回收到空闲链表

C 程序的保守标记-清除算法
Mark&Sweep 对 C 程序的垃圾收集是一种合适的方法,因为它可以就地工作,而不需要移动任何块。然而,C 语言为 isPtr 函数的实现造成了一些有趣的挑战。

第一,C 不会用任何类型信息来标记内存位置。因此,对 isPtr 没有一种明显的方式来判断它的输入参数 p 是不是一个指针。第二,即使我们知道 p 是一个指针,对 isPtr 也没有明显的方式来判断 p 是否指向一个已分配块的有效载荷中的某个位置。

对后一问题的解决方法是将已分配块集合维护成一棵平衡二叉树,这棵树保持着这样一个属性:左子树中的所有块都放在较小的地址处,而右子树中的所有块都放在较大的地址处。如图 9-53 所示,这就要求每个已分配块的头部里有两个附加字段(left 和 right)。每个字段指向某个已分配块的头部。isPtr(ptr p) 函数用树来执行对已分配块的二分查找。在每一步中,它依赖于块头部中的大小字段来判断 p 是否落在这个块的范围之内。


一棵已分配块的平衡树中的左右指针

平衡树方法保证会标记所有从根节点可达的节点,从这个意义上来说它是正确的。这是一个必要的保证,因为应用程序的用户当然不会喜欢把他们的已分配块过早地返回给空闲链表。然而,这种方法从某种意义上而言又是保守的,因为它可能不正确地标记实际上不可达的块,因此它可能不会释放某些垃圾。虽然这并不影响应用程序的正确性,但是这可能导致不必要的外部碎片。

C 程序的 Mark&Sweep 收集器必须是保守的,其根本原因是 C 语言不会用类型信息来标记内存位置。因此,像 int 或者 float 这样的标量可以伪装成指针。例如,假设某个可达的已分配块在它的有效载荷中包含一个 int,其值碰巧对应于某个其他已分配块 b 的有效载荷中的一个地址。对收集器而言,是没有办法推断出这个数据实际上是 int 而不是指针。因此,分配器必须保守地将块 b 标记为可达,尽管事实上它可能是不可达的。