前言:开通专栏后的第一篇文章,接下来将会就GO语言的内存,GC,并发编程等,深入理解GO这门语言。
一,内存模型概述
首先明确几个概念:
(1) cache:线程私有的,每次对象分配时候先从cache查询,小对象如果能获得空闲内存则不用加锁了。来看看cache的结构(省略了跟gc等相关的字段)
type mcache struct {
alloc [numSpanClasses]*mspan // 用于分配的span
spanclass spanClass // size class and noscan (uint8)
}
阅读源码我们可以看到cache有一个0到n的数组,每个数组挂载着一个链表,链表的节点代表着一个内存单元,而且同一个链表的节点的内存块都是相等的,不同链表内存大小不同(大小根据spanclass的值作为下标来对应sizeclass.go中的class_to_size数组)。接下来我们看看内存分配在这里的逻辑,我们可以找到malloc.go中的mallocgc(源码就不贴上来了),其实逻辑很简单,源码注释也很清楚,主要是判断对象是否是小对象和大对象(小对象里面还分为tiny和small)。大对象的话直接去堆中分配,小对象根据sizeclass取出一个内存块链表,然后取出该链表的可用节点。对于tiny对象的处理非常有趣,它不能为指针,因为多个tiny对象分配到一个object,无法应对垃圾扫描。
(2) Central:线程共享的,如果在cache中找不到空闲内存,那么cache就会申请一批小对象内存到本地缓存中,这个过程是需要加锁的。(注意:Central里面是一个个的page)结构如下(同样只保留了跟内存相关代码):
type mcentral struct {
spanclass spanClass
nonempty mSpanList // 有空闲内存的span列表
empty mSpanList // 无空闲内存的span列表
}
我们可以通过看malloc.go的mallocgc的代码,我们发现如果cache内存不足,那么会调用到mcache.go的refill函数,再到mcentral.go的cacheSpan函数,然后根据sizeclass大小取出相应的central获取到相应的内存块。在这一层内存管理粒度为span。
(3) Heap:线程共享的,如果Central中没有空闲的内存page,那么就会从Heap中申请内存,这个过程需要加锁。看看结构定义(列出几个重要的跟内存相关的)
type mheap struct {
free [_MaxMHeapList]mSpanList // 页数在127以内的空闲span链表数组
freelarge mTreap // 页数大于127时,转而使用treap
allspans []*mspan // 记录申请过的span
spans []*mspan // 记录arena区域页号跟mspan的映射关系
bitmap uintptr // Points to one byte past the end of the bitmap
bitmap_mapped uintptr
//还有几个跟arena相关的参数就不列举了
}
在这层中,申请内存的单位为page,从heap申请的page是连续的,通过span来管理,这块的逻辑我们可以看mheap.go的alloc_m函数。如果Central向Heap申请内存,那么接下来就会根据page的个数去取最合适的span。接下来盗一个图,觉得画的很详细:
二,内存分配器-Msapn和FixAlloc
这两个都是内存分配器的基础工具组件,在看代码时候我们经常能看到这两个,接下来就分别解释下。
(1) Mspan
这是用来管理page对象的,而且是连续的page,结构定义如下:
type mspan struct {
next *mspan // next span in list, or nil if none
prev *mspan // previous span in list, or nil if none
list *mSpanList // 1.9后计划移除的字段,不做学习
startAddr uintptr // 第一个span的地址
npages uintptr // 该span存储的page个数
}
上面给出了几个重要的字段,可以看出,span结构的next和prev指针是用来构造双向链表的,其实span的用处也只是管理一组连续的page而已,还是比较简单的
(2) FixAlloc
这是用来管理MCache和MSpan的两个特定的对象,结构定义如下:
type fixalloc struct {
size uintptr
first func(arg, p unsafe.Pointer) // called first time p is returned
arg unsafe.Pointer
list *mlink
chunk uintptr
nchunk uint32
inuse uintptr // in-use bytes now
stat *uint64
zero bool // zero allocations
}
list上是一个链表,每个节点是一个固定大小的内存块(cachealloc中的大小为sizeof(MCache),spanalloc的大小为sizeof(MSpan))。接下来我们看到mfixalloc.go中的alloc函数,逻辑大致如下:使用fixalloc分配MCache和Mspan时候,那么首先会判断list是否为空,不为空则返回一个内存块使用,如果为空,则判断chunk上有无足够的内存可用,再进行处理
三,总结
写了两天,终于写完了,还是写的很粗糙,相信随着后面的学习,能学习到更多,再回来修改。
老大说过,阅读源码后不能只停留在读源码的层面,要想想读完之后自己的收获,多思考如果是我,会怎么设计。
首先是提高了自己阅读代码的能力吧,然后学习到了treap树,还有就是作者在设计这些内存模型时候的考虑的精妙的思想,例如如何更加快速的计算sizeclass,如何避免False Sharing等问题。有些问题比如FalseSharing是一个很隐蔽的问题,但是确是非常重要的。