Skip to content

Commit

Permalink
添加了垃圾回收的分析
Browse files Browse the repository at this point in the history
  • Loading branch information
tiancaiamao committed Mar 10, 2013
1 parent 0454fd8 commit 68c0905
Show file tree
Hide file tree
Showing 4 changed files with 271 additions and 61 deletions.
192 changes: 143 additions & 49 deletions all.org
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,6 @@ go语言中使用的是栈不是连续的。原因是需要支持goroutine。分
5. 继续执行遇到RET指令时会返回到runtime.less,less做的事情跟more相反,它要准备好从newstack到old stack
整个过程有点像一次中断,中断处理时保存当时的现场,弄个新的栈,中断恢复时恢复到新栈中运行,运行到return时又要从runtime.less走回去

* 结语
本文是对splitstack的一个学习,主要是gcc中分段栈的原理和go语言的具体实现方式.
* 编译过程分析

$GOROOT/src/cmd/gc目录,这里gc不是垃圾回收的意思,而是go compiler
Expand Down Expand Up @@ -225,53 +223,6 @@ runtime.main --> main.main

-----------------------------------------------------------------------------------------------

* interface的实现

假设我们把类型分为具体类型和接口类型。

具体类型例如type myint int32 或type mytype struct {...}

接口类型是例如type I interface {}

接口类型的值,在内存中的存放形式是两个域,一个指向真实数据(具体类型的数据)的指针,一个itab指针。

具体见$GOROOT/src/pkg/reflect/value.go 的type nonEmptyInterface struct {...} 定义

itab中包含了数据(具体类型的)的类型描述符信息和一个方法表

方法表就类似于C++中的对象的虚函数表,上面存的全是函数指针。

方法表是在接口值在初始化的时候动态生成的。具体的说:

对每个具体类型,都会生成一个类型描述结构,这个类型描述结构包含了这个类型的方法列表

对接口类型,同样也生成一个类型描述结构,这个类型描述结构包含了接口的方法列表

接口值被初始化的时候,利用具体类型的方法表来动态生成接口值的方法表。

比如说var i I = mytype的过程就是:

构造一个接口类型I的值,值的第一个域是一个指针,指向mytype数据的一个副本。注意是副本而不是mytype数据本身,因为如果不这样的话改变了mytype的值,i的值也被改变。

值的第二个域是指向一个动态构造出来的itab,itab的类型描述符域是存mytype的类型描述符,itab的方法表域是将mytype的类型描述符的方法表的对应函数指针拷贝过来。构造itab的代码在$ROOT/src/pkg/runtime/iface.c中的函数

static Itab* itab(InterfaceType *inter, Type *type, int32 canfail)

这里还有个小细节是类型描述符的方法表是按方法名排序过的,这样itab的动态构建过程更快一些,复杂度就是O(接口类型方法表长度+具体类型方法表长度)

可能有人有过疑问:编译器怎么知道某个类型是否实现了某个接口呢?这里正好解决了这个疑问:

在var i I = mytype 的过程中,如果发现mytype的类型描述符中的方法表跟接口I的类型描述符中的方法表对不上,这个初始化过程就会出错,提示说mytype没有实现接口中的某某方法。

再暴一个细节,所有的方法,在编译过程中都被转换成了函数

比如说 func (s *mytype) Get()会被变成func Get(s *mytype)。

接口值进行方法调用的时候,会找到itab中的方法表的某个函数指针,其第一个参数传的正是这个接口值的第一个域,即指向具体类型数据的指针。

在具体实现上面还有一些优化过程,比如接口值的真实数据指针那个域,如果真实数据大小是32位,就不用存指针了,直接存数据本身。再有就是对类接口类型interface{},其itab中是不需要方法表的,所以这里不是itab而直接是一个指向真实数据的类型描述结构的指针。

-------------------------------------------------------------------------------------------------
* 调度器
** 总体介绍
$GOROOT/src/pkg/runtime目录很重要,值得好好研究,源代码可以从runtime.h开始读起。
Expand Down Expand Up @@ -475,7 +426,144 @@ MCentral层次是作为MCache和MHeap的连接。对上,它从MHeap中申请MS
} central[NumSizeClasses];
#+end_src
* 垃圾回收
这里假设读者对mark-sweep的垃圾回收算法有基本的了解,否则没办法读懂这部分的代码。
** 位图标记和内存布局
目前go中的垃圾回收用的是标记清扫法.保守的垃圾回收,进行回收时会stoptheworld.

每个机器字节(32位或64位)会对应4位的标记位.因此相当于64位系统中每个标记位图的字节对应16个堆字节.

字节中的位先根据类型,再根据堆中的分配位置进行打包,因此每个64位的标记位图从上到下依次包括:\\
#+begin_quote
16位特殊位,对应堆字节\\
16位垃圾回收的标记位\\
16字节的 无指针/块边界 的标记位
16位的 已分配 标记位\\
#+end_quote
这样设计使得对一个类型的相应的位进行遍历很容易.

地址与它们的标记位图是分开存储和.以mheap.arena_start地址为边界,向上是实际使用的地址空间,向下是标记位图.比如在64位系统中,计算某个地址的标记位的公式如下:
#+begin_quote
偏移 = 地址 - mheap.arena_start\\
标记位地址 = mheap.arena_start - 偏移/16 - 1 (32位中是偏移/8,就是每标记字节对应多少机器字节)\\
移位 = 偏移 % 16
标记位 = *标记位地址 >> 移位
#+end_quote
然后就可以通过 (标记位 & 垃圾回收标记位),(标记位 & 分配位),等来测试相应的位.
其中已分配的标记为1<<0,无指针/块边界是1<<16,垃圾回收的标记位为1<<32,特殊位1<<48

内存布局如下图所示:
../image/gc_bitmap.jpg

** 基本的mark过程
go的垃圾回收还不是很完善.相应的代码在mgc0.c,可以看到这部分的代码质量相对其它部分是明显做得比较糙的.比如反复出现的模块都没写个函数:
#+begin_src c
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;
#+end_src
再比如说markallocated和markspan,markfreed做的事情都差不多一样的,却写了三个函数.
由于代码写得不行,所以读得出吃力一些.先抛开这些不谈,还是从最简单的开始看,mark过程,从debug_scanblock开始读,这个跟普通的标记-清扫的垃圾回收算法结构是一样的.

debug_scanblock函数是递归实现的,单线程的,更简单更慢的scanblock版本.该函数接收的参数分别是一个指针表示要扫描的地址,以及字节数.

首先要将传入的地址,按机器字节大小对应.\\
然后对待扫描区域的每个地址:\\
找到它所在的MSpan,再找到该地址在MSpan中所处的对象地址(内存管理中分析过,go中的内存池中的小对象).\\
既然有了对象的地址,则根据它找到对应位图里的标记位.前一小节已经写了从地址到标记位图的转换过程.\\
判断标记位,如果是未分配则跳过.否则打上特殊位标记(debug_scanblock中用特殊位代码的mark位)完成标记.\\
还要判断标记位中是否含有无指针的标记位,如果没有,则还要递归地调用debug_scanblock.

如果对mark-sweep算法有点基础,读debug_scanblock应该不难理解。
** 并行的垃圾回收操作
整个的gc是以runtime.gc函数为入口的,它实际调用的是gc.进入gc后会先stoptheworld.接着添加标记的root.
然后会设置markroot和sweepspan的并行任务。
运行mark的任务,扫描块,运行sweep的任务,最后starttheworld并切换出去。

总体来讲现在版本的go中的垃圾回收是设计成多线程合作完成的,有个parfor.c文件中有相应代码。以前版本是单线程做的。在gc函数中调用了
#+begin_src c
runtime·parforsetup(work.markfor, work.nproc, work.nroot, nil, false, markroot);
runtime·parforsetup(work.sweepfor, work.nproc, runtime·mheap->nspan, nil, true, sweepspan);
#+end_src
是设置好回调让线程去执行markroot和sweepspan函数。

实现方式就是设置一个工作缓存,原来debug_scanblock中是遇到一个新的指针就递归地调用处理,而现在是遇到一个新的指针就进队列加到工作缓存中。
功能上差不多,一个是非递归一个是递归。scanblock从工作区开始扫描,扫描到的加个mark标记,如果遇到可能的指针,不是递归处理而是加到工作队列中。这样可以多个线程同时进行。
并行设计中,有设置工作区的概念,多个worker同时去工作缓存中取数据出来处理,如果自己的任务做完了,就会从其它的任务中“偷”一些过来执行。

** 虚拟机
scanblock函数非常难读,我觉得应该好好重构一下。上面有两个大的循环,第一个作用是对整个扫描块区域,将类型信息提取出来。另一个大循环是实现一个虚拟机操作码的解析执行。

为什么会弄个虚拟机呢?目前我也不明白为啥这么搞。反正垃圾回收的操作都被弄成了操作码,用虚拟机去解释执行的。不同类型的对象,由于垃圾回收的方式不一样,把各种类型的回收操作独立出来做成操作码,可能是灵活度更大吧。

go是这样弄的啊:
从一个地址可以找到相应的标记位图。\\
过程是通过地址到MSpan,然后MSpan->type.compression得到一个type的描述\\
再由type描述得到类型信息\\
类型信息是一个Type结构体(在type.h头文件中定义),其中有个void *gc域\\
gc其实就是代码段了。通过虚拟机解释其中的操作码完成各种类型的对象的垃圾回收操作。

回收ptr,slice,string...不同类型都会对应到不同的操作码。其中也有一些小技巧的东西比如type描述符。它是一个uintptr,由于内存分配是机器字节对齐的,所以地址就只用到了高位。type描述符中高位存放的是Type结构体的指针,低位可以用来存放类型。通过
#+begin_src c
t = (Type*)(type & ~(uintptr)(PtrSize-1));
#+end_src
就可以从type的描述符得到Type结构体,而通过
#+begin_src c
type & (PtrSize-1)
#+end_src
就可以得到类型。

gc的触发是由一个gcpercent的变量控制的,当新分配的内存占已在使用中的内存的比例超过gcprecent时就会触发.比如说gcpercent=100,当前使用了4M,当内存分配到达8M时就会再次gc.
* 类型系统
** chan的实现
** interface的实现

假设我们把类型分为具体类型和接口类型。

具体类型例如type myint int32 或type mytype struct {...}

接口类型是例如type I interface {}

接口类型的值,在内存中的存放形式是两个域,一个指向真实数据(具体类型的数据)的指针,一个itab指针。

具体见$GOROOT/src/pkg/reflect/value.go 的type nonEmptyInterface struct {...} 定义

itab中包含了数据(具体类型的)的类型描述符信息和一个方法表

方法表就类似于C++中的对象的虚函数表,上面存的全是函数指针。

方法表是在接口值在初始化的时候动态生成的。具体的说:

对每个具体类型,都会生成一个类型描述结构,这个类型描述结构包含了这个类型的方法列表

对接口类型,同样也生成一个类型描述结构,这个类型描述结构包含了接口的方法列表

接口值被初始化的时候,利用具体类型的方法表来动态生成接口值的方法表。

比如说var i I = mytype的过程就是:

构造一个接口类型I的值,值的第一个域是一个指针,指向mytype数据的一个副本。注意是副本而不是mytype数据本身,因为如果不这样的话改变了mytype的值,i的值也被改变。

值的第二个域是指向一个动态构造出来的itab,itab的类型描述符域是存mytype的类型描述符,itab的方法表域是将mytype的类型描述符的方法表的对应函数指针拷贝过来。构造itab的代码在$ROOT/src/pkg/runtime/iface.c中的函数

static Itab* itab(InterfaceType *inter, Type *type, int32 canfail)

这里还有个小细节是类型描述符的方法表是按方法名排序过的,这样itab的动态构建过程更快一些,复杂度就是O(接口类型方法表长度+具体类型方法表长度)

可能有人有过疑问:编译器怎么知道某个类型是否实现了某个接口呢?这里正好解决了这个疑问:

在var i I = mytype 的过程中,如果发现mytype的类型描述符中的方法表跟接口I的类型描述符中的方法表对不上,这个初始化过程就会出错,提示说mytype没有实现接口中的某某方法。

再暴一个细节,所有的方法,在编译过程中都被转换成了函数

比如说 func (s *mytype) Get()会被变成func Get(s *mytype)。

接口值进行方法调用的时候,会找到itab中的方法表的某个函数指针,其第一个参数传的正是这个接口值的第一个域,即指向具体类型数据的指针。

在具体实现上面还有一些优化过程,比如接口值的真实数据指针那个域,如果真实数据大小是32位,就不用存指针了,直接存数据本身。再有就是对类接口类型interface{},其itab中是不需要方法表的,所以这里不是itab而直接是一个指向真实数据的类型描述结构的指针。

-------------------------------------------------------------------------------------------------
* 收集的一些关于go internals的链接:

http://code.google.com/p/try-catch-finally/wiki/GoInternals
Expand All @@ -491,3 +579,9 @@ http://blog.csdn.net/hopingwhite/article/details/5782888
http://www.douban.com/note/251142022/ 调度器

http://shiningray.cn/tcmalloc-thread-caching-malloc.html tcmalloc内存管理

方法原码分析
1独立的函数
2对象怎样调用方法
3组合对象(或接口)后怎样调用方法
4接口怎样调用方法
Binary file added image/gc_bitmap.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 68c0905

Please sign in to comment.