MarkSweep 即 标记 - 清除算法
MarkSweep 从名字就能知道,此算法包含两个阶段
- 标记阶段,负责将所有活动对象打上标记
- 清除阶段,负责将没有被标记的对象回收
标记阶段从 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
}
}
}
优点
- 简单
- 与保守式 GC 兼容
缺点
- 碎片化
- 分配速度慢
- 与写时复制不兼容
这里重点记录对缺点的分析
先补充下堆内存的分配机制
内存的分配
分配就是将之前回收的块进行再利用。这里要解决的问题就是如何选择合适的块。如果找不到合适的块则报错或者扩大堆(暂时不讨论扩大堆),分配机制有以下三种
- First-fit 使用第一个能满足的分块
- Best-fit 遍历空间,使用能满足且最靠近的返回
- Worst-fit 使用最大的分块
这里的使用并不是进行返回,而是针对这个块进行进一步操作
- 如果大小正好,则返回
- 如果大了,就进行切割,返回合适的,将剩余的返回给空闲列表
碎片化
考虑到内存的分配机制,就可以知道碎片化会导致内存利用率低。
打个比方,一块连续空间被 GC 多次以后,可能存在斑马线的情况,相邻空间等大,且有一块闲置。如果此时需要分配两个块的空间,那么上述的分配机制都无法满足。这样就出现了 『虽然有很多内存,但是无法分配』的情况。
改进措施有
- Compact
- BiBOP
分配速度慢
有碎片,那么空余空间就不连续,也就是能立刻寻找到合适的内存块的几率降低。极端情况需要遍历整个空闲列表。
改进措施有
- 多空闲链表 multiple free-list
- BiBOP
与写时复制技术不兼容
写时复制 是共享内存的重要手段,能大大减少内存的不必要消耗,仅当共享内存区域发生变化的时候,才会将该部分空间进行拷贝,供修改方使用。那么由于标记阶段会修改标记位,从而变更内存,就会触发此处的写时复制机制,GC 本是释放空间,但是因此反倒可能造成对内存空间的压迫。
改进措施有
- 位图标记 bitmap marking
算法改进
- Compact(改善碎片化)
- Multiple Free-List(改善碎片化,改善分配速度慢)
- BiBOP(改善碎片化,改善分配速度慢)
- Bitmap Marking(改善与写时复制技术的不兼容)
- 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
既然标记阶段修改所有对象的头部与写时复制不兼容,那么就将此修改抽出并集中到其他地方,这样就可以保持兼容了。位图标记的思路就是如此,使用表格来维护对象的标记状态。
使用表格有两个好处
- 写时复制技术兼容
- 清除操作更高效,因为避免了堆的遍历,使用表格进行即可
LazySweep
在原生算法中,清除阶段耗时与堆大小成正比,可能会造成较长的暂停时间。那么将清除阶段按需停止,只要能得到适合大小的空间就停止,既能满足内存分配需要,也能减少暂定时间。这个就是 lazySweep 的设计思路。当然极端情况下可能仍旧需要遍历整个堆。这一点与后面要说的间隔执行有些相似。
但是 lazySweep 的效果并不稳定。 因为 lazy 操作需要有 偏移量,来保存下次启动回收的位置,假如这个位置在活动对象密集处,那么这个暂停时间反倒会拉长。
再就是,lazySweep 没有考虑标记阶段。这个问题,都会在间隔执行处进行解决