(阅读笔记)垃圾回收的算法与实现 - Mark Sweep

本文详细介绍了垃圾回收中的MarkSweep算法,包括其基本原理、优点和缺点。着重分析了碎片化、分配速度慢以及与写时复制技术不兼容等问题,并探讨了Compact、Multiple Free-List、BiBOP、Bitmap Marking和LazySweep等改进策略,以提高内存利用率和分配效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

MarkSweep 即 标记 - 清除算法

MarkSweep 从名字就能知道,此算法包含两个阶段

  1. 标记阶段,负责将所有活动对象打上标记
  2. 清除阶段,负责将没有被标记的对象回收

标记阶段从 root 进行出发扫描,根据引用关系寻找到所有对象并打标记。然后清除阶段则是从堆头部向后扫描对象,将非活动对象进行回收,以待下次分配。这个就是该算法的逻辑执行过程。

基础实现

基础实现是上述逻辑过程的一个简单实现

代码

// 头部结构
type header struct {
	Marked bool
	Size int
}

// 对象
type object struct {
	Header   header
	Field    interface{}
	Children []*object
}

// 根,此处是抽象概念,根包含全局变量空间,调用栈等
func markPhrase() {
	for _, o := range root {
		mark(o)
	}
}

// 标记代码,此处是一个递归标记
func mark(obj *object) {
	if !obj.Header.Marked && obj.Next != nil {
		mark(obj.Next)
	}
	obj.Header.Marked = true
}

// 清理阶段,扫描堆空间,回收未标记的空间,然后将已有的标记位重置
func sweepPhrase(){
    for offset := 0; offset< heapSize;  {
        if !heap[i].Header.Marked{
            mem[i].Next = freeList
            freeList = mem[i]
            offset+=heap[i].Header.Marked.Size
        } else {
            mem[i].Header.Marked  = false
        }
    }
}

优点

  1. 简单
  2. 与保守式 GC 兼容

缺点

  1. 碎片化
  2. 分配速度慢
  3. 与写时复制不兼容

这里重点记录对缺点的分析

先补充下堆内存的分配机制

内存的分配

分配就是将之前回收的块进行再利用。这里要解决的问题就是如何选择合适的块。如果找不到合适的块则报错或者扩大堆(暂时不讨论扩大堆),分配机制有以下三种

  • First-fit 使用第一个能满足的分块
  • Best-fit 遍历空间,使用能满足且最靠近的返回
  • Worst-fit 使用最大的分块

这里的使用并不是进行返回,而是针对这个块进行进一步操作

  1. 如果大小正好,则返回
  2. 如果大了,就进行切割,返回合适的,将剩余的返回给空闲列表
碎片化

考虑到内存的分配机制,就可以知道碎片化会导致内存利用率低。

打个比方,一块连续空间被 GC 多次以后,可能存在斑马线的情况,相邻空间等大,且有一块闲置。如果此时需要分配两个块的空间,那么上述的分配机制都无法满足。这样就出现了 『虽然有很多内存,但是无法分配』的情况。

改进措施有

  1. Compact
  2. BiBOP
分配速度慢

有碎片,那么空余空间就不连续,也就是能立刻寻找到合适的内存块的几率降低。极端情况需要遍历整个空闲列表。

改进措施有

  1. 多空闲链表 multiple free-list
  2. BiBOP
与写时复制技术不兼容

写时复制 是共享内存的重要手段,能大大减少内存的不必要消耗,仅当共享内存区域发生变化的时候,才会将该部分空间进行拷贝,供修改方使用。那么由于标记阶段会修改标记位,从而变更内存,就会触发此处的写时复制机制,GC 本是释放空间,但是因此反倒可能造成对内存空间的压迫。

改进措施有

  1. 位图标记 bitmap marking

算法改进

  1. Compact(改善碎片化)
  2. Multiple Free-List(改善碎片化,改善分配速度慢)
  3. BiBOP(改善碎片化,改善分配速度慢)
  4. Bitmap Marking(改善与写时复制技术的不兼容)
  5. LazySweep (改善最大暂停时间)

Compact

压缩是将活跃对象移动到集中的区域(这里的移动其实就是复制机制,也就是之后要说的复制算法),不过这已经超出 MarkSweep 的范畴,此处不再赘述。

Multiple Free-List

多空闲链表机制是块分组机制,按块大小进行分组,组成多个链表。获取块时,优先从大小正好的空间进行分配,回收后也释放到原来的链表中。从而避免遍历,加快分配速度。

那么此处的问题就是 需要多少个空闲链表,通常会给分块大小设定一个上限,分块如果大于等于这个大小,就全部采用 一个空闲链表处理。 考虑到超限申请是极为罕见,所以效率低一些是可接受的。

BiBOP

BiBOP = Big Bag Of Pages

碎片化的原因之一是堆上杂乱分布着大小各异的对象。BiBOP 设计的出发点就是基于此

BiBOP 将内存分为固定大小的块,固定数量的块叫做一个 Page,几个 Page 的集合叫做一个 Bag(印象中 memcache 的内存分布就是这样的)

然后根据对象大小,分配到指定的 Bag 中,这样即使回收,将来也可以再重新填充一个相同大小的对象进来,这一点,思路与 Multiple Free-List 类似。那么与 Multiple Free-List 一样,同样面对着可能降低堆内存使用率的问题。

Bitmap Marking

既然标记阶段修改所有对象的头部与写时复制不兼容,那么就将此修改抽出并集中到其他地方,这样就可以保持兼容了。位图标记的思路就是如此,使用表格来维护对象的标记状态。

使用表格有两个好处

  1. 写时复制技术兼容
  2. 清除操作更高效,因为避免了堆的遍历,使用表格进行即可

LazySweep

在原生算法中,清除阶段耗时与堆大小成正比,可能会造成较长的暂停时间。那么将清除阶段按需停止,只要能得到适合大小的空间就停止,既能满足内存分配需要,也能减少暂定时间。这个就是 lazySweep 的设计思路。当然极端情况下可能仍旧需要遍历整个堆。这一点与后面要说的间隔执行有些相似。

但是 lazySweep 的效果并不稳定。 因为 lazy 操作需要有 偏移量,来保存下次启动回收的位置,假如这个位置在活动对象密集处,那么这个暂停时间反倒会拉长。

再就是,lazySweep 没有考虑标记阶段。这个问题,都会在间隔执行处进行解决

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值