GC标记-清除算法
分为两个阶段
- 标记阶段:把所有活动对象做上标记的阶段。
- 清除阶段:把那些没有标记的对象(非活动对象)回收的阶段。
通过这两个阶段就可以令不能利用的内存空间重新得到利用。
伪代码如下:
mark_sweep() {
mark_phase() //标记阶段
sweep_phase() //清除阶段
}
假设我们执行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 //移动指针到堆中下一个对象的头
}
}
相关说明:
-
sweeping
: 用来遍历堆的指针,每移动一次增加当前sweeping.size
个大小。 -
size
域: 存储对象大小(字节数)的域,与mark域一样,事先定义在对象头中。 -
$free_list
: 名为空闲链表的单向链表,遍历堆时会将非活动对象加入到空闲链表中。后续分配新对象时,通过遍历这个空闲链表来找到分块。 -
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
大小后剩余大小的分块,并把剩余的分块返回空闲链表。 - 如果没有找到合适的分块,则抛出分配错误。