GC-标记清除算法(mark-sweep)

前一篇-GC算法基础相关概念

GC标记-清除算法

分为两个阶段

  • 标记阶段:把所有活动对象做上标记的阶段。
  • 清除阶段:把那些没有标记的对象(非活动对象)回收的阶段。

通过这两个阶段就可以令不能利用的内存空间重新得到利用。

伪代码如下:

mark_sweep() {
    mark_phase() //标记阶段
    sweep_phase() //清除阶段
}

假设我们执行GC前堆的状态示意图如下:


执行GC前堆的状态

标记阶段

简单概况: 标记阶段就是“遍历对象并标记”的处理过程

伪代码如下:

mark_phase() {
    for(r: $roots) {
        mark(*r)
    }
}

标记阶段中,为所有活动对象打上标记。其中活动对象是通过遍历、递归根(roots)直接引用的对象。

mark() 函数功能如下:

mark(obj) {
    if(obj.mark == FALSE) { //检查对象标记状态
        obj.mark = TRUE     //将标记置为TRUE
        for (child : children(obj)) {
            mark(*child) //递归标记引用的对象
        }
    }
}

标记操作会对对象中的mark标志位进行处理:

设置标志位的处理

标记阶段结束后,堆的状态如下:

标记阶段结束后状态

清除阶段

清除阶段会遍历整个堆,并回收没有打上标记的对象(垃圾对象)。使其能再次得到利用。

伪代码:

sweep_phase() {
    sweeping = $heap_start //堆起始位置
    while(sweeping < $heap_end) { //遍历堆
        if(sweeping.mark == TRUE) {
            sweeping.mark = FALSE   // 重置标记状态
        } else {
            sweeping.next = $free_list //加入到空闲链表
            $free_list = sweeping
        }
        sweeping += sweeping.size //移动指针到堆中下一个对象的头
    }
}

相关说明:

  1. sweeping: 用来遍历堆的指针,每移动一次增加当前sweeping.size个大小。
  2. size域: 存储对象大小(字节数)的域,与mark域一样,事先定义在对象头中。
  3. $free_list: 名为空闲链表的单向链表,遍历堆时会将非活动对象加入到空闲链表中。后续分配新对象时,通过遍历这个空闲链表来找到分块。
  4. next域: 只有在生成空闲链表及从空闲链表中取出分块时才会使用到,因此不需要额外定义一个next域,实际上是对象最初始的域(field1)。即给field1取的别名为next。与C语言中union 联合体概念相同。 (有点难于理解)

清除阶段完成后堆状态如下:

清除阶段结束后堆状态

总结: 清除阶段程序会遍历所有堆,进行垃圾回收。因此,花费的事件与堆大小成正比。堆越大,清除阶段花费的时间越长。

分配

分配指将回收的垃圾进行再利用。即当mutator申请分块时,将合适大小的分块分配给muator。实际操作是通过搜索空闲链表并寻找大小合适的分块。

伪代码:

new_obj(size) {
    chunk = pick_chunk(size, &free_list) {
        if(chunk != NULL) {
            return chunk //返回分块
        }
        else {
            allocation_fail() //分块失败
        }
    }
}

其中pick_chunk()函数用于遍历$free_list空闲链表,寻找大于等于size的分块。

  • 如果找到和size大小相同的分块,则会直接返回该分块;
  • 如果找到比size大的分块,则将其分割成size大小的分块和去掉size大小后剩余大小的分块,并把剩余的分块返回空闲链表。
  • 如果没有找到合适的分块,则抛出分配错误。

推荐阅读更多精彩内容