垃圾回收 -标记清除算法

就如他的字面意思一样,由标记阶段和清除阶段构成。标记阶段是把所有的活动对象都做上标记的阶段。清除阶段是把那些没有标记的对象,也就是非活动对象回收的阶段。通过这两个阶段,就可以令不能利用的内存空间重新得到利用。

1、 标记阶段

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

在标记阶段中,collector会为堆里所有活动对象打上标记。为此,我们首先要标记通过根直接引用的对象。然后递归地标记通过指针数组能访问到的对象。这样就能把所有活动对象都标记上。

mark(obj){
	if(obj.mark == false)
		obj.mark = true
		for(child :children(obj))
			mark(*child)
}

如果标记未完成,则程序会在对象头部进行置位操作。这个位要分配在对象的头之中,并且能用obj.mark访问。
在这里插入图片描述

在标记阶段,程序会标记所有活动对象。毫无疑问,标记所花费的时间是与“活动对象的总数成正比”。
标记阶段结束时的堆如下图所示:

在这里插入图片描述

2、 清除阶段

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

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
}

在此出现了叫做size的域,这是存储对象大小(字节数)的域,跟mark域一样,我们事先在各对象的头中定义他们。
在清除阶段,我们使用变量sweeping遍历堆,具体来说就是从堆首地址$heap_start开始,按顺序一个个遍历对象的标志位。
设置了标志位,就说明这个对象是活动对象。活动对象必然是不能回收的。因为我们取消标志位,准备下一次的GC。
我们必须把非活动对象回收再利用。回收对象就是把对象分块,链接到被称为“空闲链表”的单向链表。在之后进行分配时只要遍历这个空闲链表,就可以找到分块了。
在清除阶段,程序会遍历所有堆,进行垃圾回收。也就是说,所花费时间与堆大小成正比。堆越大,清楚阶段所花费的时间就会越长。
在这里插入图片描述

3、分配

接下来为大家讲解分配的相关内容。这里的分配是指将回收的垃圾进行再利用。那么,分配是怎样进行的呢?也就是说,当mutator 申请分块时,怎样才能把大小合适的分块分配给mutator 呢?
如前文所述,我们在清除阶段已经把垃圾对象连接到空闲链表了。搜素空闲链表并寻找大小合适的分块,这项操作就叫作分配。执行分配的函数 new_obj()如下所示。

new_obj(size){
	chunk = pickup_chunk(size,$free_list)
	if(chunk != NULL) return chunk
	else allocation_fail()
}

这里的pickup_chunk()函数用于遍历$free_list,寻找大于等于size的分块。如果他找到和size大小相同的分块,则会直接返回改分块;如果找到比size大的分块,则会将其分割成size大小的分块和去掉size后剩余大小的分块,并把剩余的分块返回给空闲链表。

分配策略描述
First-fit发现大于等于size的分块立即返回结果
Worst-fit找出大于等于size的最大分块返回结果
Best-fit找出大于等于size的最小分块返回结果

4、合并

根据分配策略的不同可能会产生大量的小分块。但如果他们是连续的,我们就能把所有的小分块连在一起形成一个大分块。这种操作就叫做合并。

sweep_phase(){
	sweeping = $heap_start
	while(sweeping < $heap_end)
		if(sweeping.mark == true) sweeping.mark  = false
		else
			if(sweeping == $free_list + $free_list.size)
				$free_list.size += sweeping.size
			else
				sweeping.next = $free_list
				$free_list = sweeping
			sweeping += sweeping.size
}

5、优缺点

优点

  • 实现简单
  • 与保守式GC算法兼容:不会移动对象

缺点

  • 碎片化:使用过程中会逐渐产生被细化的分块,不久后就会导致无数的小分块散布在堆的各处。众所周知,Windows的文件系统也会产生这种现象。
  • 分配速度底下:因为分块不是连续的,每次分配都必须遍历空闲链表,找到足够大的分块。最糟糕的情况就是每次进行分配都得把空闲链表遍历到最后。下面将要介绍的多个空闲链表和BiBOP法都是为了能在标记清除算法中高速进行分配而想出的方法。
  • 与写时复制技术不兼容:即使没有重写对象,GC也会设置所有活动对象的标志位,这样就会频繁发生本不应该发生的复制,压迫到内存空间。

写时复制技术(copy-on-write)
在Linux等众多UNIX操作系统的虚拟存储中用的的高速化方法。比如:在Linux中复制进程,也就是使用fork函数时,大部分内存空间都不会被复制。只是复制进程,就复制了所有内存空间不太实际。因此写时复制技术只是装作复制了内存空间,实际上是将内存空间共享了。
在各个进程中访问数据时,能够访问共享内存就没什么问题了。
然而,当我们对共享内存空间进行写入时,不能重写共享内存。因为从其他程序访问时,会发生数据不一致的情况。在重写时,要复制自己私有空间的数据,对这个私有空间进行重写。复制后只访问这个私有空间,不访问贡献内存。像这样,因为这门技术是在写入时进行复制的,所以才称为写时复制技术。

6、多个空闲链表

之前的标记清除算法,我们只用到了一个空闲链表,在这个空闲链表中,对大的分块和小的分块进行了同样的处理,但这样一来,每次分配的时候都要遍历一次空闲链表来寻找合适大小的空闲链表。
因此,我们就利用分块大小不同的空闲链表,来分配不同的分块。
在这里插入图片描述

7、BiBOP法

Big Bag Of Page:将大小相近的对象整理成固定大小的块进行管理的做法。
我们把堆分割成固定大小的块,让每个块只能配置同样大小的对象。
在这里插入图片描述

BiBOP法原本是为了消除碎片化,提高堆效率而采用的方法,但像上面这样,在多个块中分散残留着同样大小的对象,反而会降低堆的使用效率。

8、位图标记

在单纯的标记清除法中,用于标记的位是被分配到各个对象的头中的,也就是说,算法是把对象和头一并处理的。所以这和写时复制技术不兼容。
对此我们有个方法,那就是只收集各个对象的标志位并表格化,不和对象一起管理,在标记的时候不在对象的头里置位,而是在这个表格中的特定场所置位。像这样集合了用于标记的位的表哥称为“位图表格”,利用这个表格进行标记的行为称为“位图标记”
在这里插入图片描述
这样做的优点就是与写时复制技术相兼容,不会产生无谓的复制。并且清除起来也更加高效。

9、延迟清除法

之前我们提到过,清除操作所花费的时间与堆大小成正比。也就是说堆越大,清除所花费的时间就越长。结果就会妨碍到mutator的处理。
延迟清除法,就是在标记操作结束后,不一并进行清除操作,而是延迟,来防止mutator长时间的暂停。
因为延迟清除法,不是一下遍历整个堆,他只在分配时执行必要的便利,所以可以压缩因清除操作导致的mutator的暂停时间。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

全栈游戏开发

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值