垃圾回收
Go语言中使用的垃圾回收使用的是标记清扫算法。进行垃圾回收时会stoptheworld。不过,在当前1.3版本中,实现了精确的垃圾回收和并行的垃圾回收,大大地提高了垃圾回收的速度,进行垃圾回收时系统并不会长时间卡住。
标记清扫算法
标记清扫算法是一个很基础的垃圾回收算法,该算法中有一个标记初始的root区域,以及一个受控堆区。root区域主要是程序运行到当前时刻的栈和全局数据区域。在受控堆区中,很多数据是程序以后不需要用到的,这类数据就可以被当作垃圾回收了。判断一个对象是否为垃圾,就是看从root区域的对象是否有直接或间接的引用到这个对象。如果没有任何对象引用到它,则说明它没有被使用,因此可以安全地当作垃圾回收掉。
标记清扫算法分为两阶段:标记阶段和清扫阶段。标记阶段,从root区域出发,扫描所有root区域的对象直接或间接引用到的对象,将这些对上全部加上标记。在回收阶段,扫描整个堆区,对所有无标记的对象进行回收。
位图标记和内存布局
既然垃圾回收算法要求给对象加上垃圾回收的标记,显然是需要有标记位的。一般的做法会将对象结构体中加上一个标记域,一些优化的做法会利用对象指针的低位进行标记,这都只是些奇技淫巧罢了。Go没有这么做,它的对象和C的结构体对象完全一致,使用的是非侵入式的标记位,我们看看它是怎么实现的。
堆区域对应了一个标记位图区域,堆中每个字(不是byte,而是word)都会在标记位区域中有对应的标记位。每个机器字(32位或64位)会对应4位的标记位。因此,64位系统中相当于每个标记位图的字节对应16个堆中的字节。
虽然是一个堆字节对应4位标记位,但标记位图区域的内存布局并不是按4位一组,而是16个堆字节为一组,将它们的标记位信息打包存储的。每组64位的标记位图从上到下依次包括:
16位的 特殊位 标记位
16位的 垃圾回收 标记位
16位的 无指针/块边界 的标记位
16位的 已分配 标记位
这样设计使得对一个类型的相应的位进行遍历很容易。
前面提到堆区域和堆地址的标记位图区域是分开存储的,其实它们是以mheap.arena_start地址为边界,向上是实际使用的堆地址空间,向下则是标记位图区域。以64位系统为例,计算堆中某个地址的标记位的公式如下:
偏移 = 地址 - mheap.arena_start
标记位地址 = mheap.arena_start - 偏移/16 - 1
移位 = 偏移 % 16
标记位 = *标记位地址 >> 移位
然后就可以通过 (标记位 & 垃圾回收标记位),(标记位 & 分配位),等来测试相应的位。其中已分配的标记为1<<0,无指针/块边界是1<<16,垃圾回收的标记位为1<<32,特殊位1<<48
三色标记的原理如下:
整个进程空间里申请每个对象占据的内存可以视为一个图, 初始状态下每个内存对象都是白色标记,先stop the world,将扫描任务作为多个并发的goroutine立即入队给调度器,进而被CPU处理,第一轮先扫描所有可达的内存对象,标记为灰色放入队列;第二轮可以恢复start the world,将第一步队列中的对象引用的对象置为灰色加入队列,一个对象引用的所有对象都置灰并加入队列后,这个对象才能置为黑色并从队列之中取出。循环往复,最后队列为空时,整个图剩下的白色内存空间即不可到达的对象,即没有被引用的对象; 第三轮再次stop the world,将第二轮过程中新增对象申请的内存进行标记(灰色),这里使用了writebarrier(写屏障)去记录这些内存的身份;
三色标记法是一个逻辑上的抽象,将对象分成白:未搜索,灰:正搜索,黑:已搜索。