go源码分析之垃圾回收
go的垃圾回收还不是很完善.相应的代码在mgc0.c,可以看到这部分的代码质量相对其它部分是明显做得比较糙的。
这里假设读者对mark-sweep的垃圾回收算法有基本的了解,否则没办法读懂这部分的代码。
位图标记和内存布局
目前go中的垃圾回收用的是标记清扫法.保守的垃圾回收(目前应该正朝精确的垃圾回收方向进行着改进),进行回收时会stoptheworld。
每个机器字节(32位或64位)会对应4位的标记位.因此相当于64位系统中每个标记位图的字节对应16个堆字节。
字节中的位先根据类型,再根据堆中的分配位置进行打包,因此每个64位的标记位图从上到下依次包括:
16位特殊位,对应堆字节
16位垃圾回收的标记位
16字节的 无指针/块边界 的标记位
16位的 已分配 标记位
偏移 = 地址 - mheap.arena_start\\
标记位地址 = mheap.arena_start - 偏移/16 - 1 (32位中是偏移/8,就是每标记字节对应多少机器字节)\\
移位 = 偏移 % 16
标记位 = *标记位地址 >> 移位
然后就可以通过 (标记位 & 垃圾回收标记位),(标记位 & 分配位),等来测试相应的位.
其中已分配的标记为1<<0,无指针/块边界是1<<16,垃圾回收的标记位为1<<32,特殊位1<<48
off = (uintptr*)obj - (uintptr*)runtime·mheap->arena_start;
bitp = (uintptr*)runtime·mheap->arena_start - off/wordsPerBitmapWord - 1;
shift = off % wordsPerBitmapWord;
xbits = *bitp;
bits = xbits >> shift;
基本的mark过程
找到它所在的MSpan,再找到该地址在MSpan中所处的对象地址(内存管理中分析过,go中的内存池中的小对象).
既然有了对象的地址,则根据它找到对应位图里的标记位.前一小节已经写了从地址到标记位图的转换过程.
判断标记位,如果是未分配则跳过.否则打上特殊位标记(debug_scanblock中用特殊位代码的mark位)完成标记.
还要判断标记位中是否含有无指针的标记位,如果没有,则还要递归地调用debug_scanblock.
并行的垃圾回收
整个的gc是以runtime.gc函数为入口的,它实际调用的是gc.进入gc后会先stoptheworld.接着添加标记的root.然后会设置markroot和sweepspan的并行任务。
运行mark的任务,扫描块,运行sweep的任务,最后starttheworld并切换出去。
总体来讲现在版本的go中的垃圾回收是设计成多线程合作完成的,在parfor.c文件中有相应代码。以前版本是单线程做的没有parfor.c文件。
在gc函数中调用了
runtime·parforsetup(work.markfor, work.nproc, work.nroot, nil, false , markroot);
runtime·parforsetup(work.sweepfor, work.nproc, runtime·mheap->nspan, nil, true , sweepspan);
是设置好回调让线程去执行markroot和sweepspan函数。
实现方式就是设置一个工作缓存,原来debug_scanblock中是遇到一个新的指针就递归地调用处理,而现在是遇到一个新的指针就进队列加入到工作缓存中。
功能上差不多,一个是非递归一个是递归。scanblock从工作区开始扫描,扫描到的加个mark标记,如果遇到可能的指针,加到工作队列中。这样可以多个线程同时进行。
并行设计中,有设置工作区的概念,多个worker同时去工作缓存中取数据出来处理,如果自己的任务做完了,就会从其它的任务中“偷”一些过来执行。
精确的垃圾回收
t = (Type*)(type & ~(uintptr)(PtrSize-1));
|
就可以得到类型。
gc的触发是由一个gcpercent的变量控制的,当新分配的内存占已在使用中的内存的比例超过gcprecent时就会触发.比如说gcpercent=100,当前使用了4M,当内存分配到达8M时就会再次gc.
还有好多好多的细节,不过这几天把大概的东西都弄得差不多了。
来源:https://my.oschina.net/u/4628563/blog/4793336