内容简介:一个程序要运行起来,操作系统会分配一块很大的虚拟内存(或者说虚拟空间)供使用,程序实际可能只使用很小的物理内存。可以通过虚拟内存空间一般会分为代码段,数据段,堆,栈等:
前一篇 讲了 Go 的调度机制和相关源码,这里说一下内存的管理,代码片段也都是基于Go 1.12。
简要的背景
一个程序要运行起来,操作系统会分配一块很大的虚拟内存(或者说虚拟空间)供使用,程序实际可能只使用很小的物理内存。可以通过 ps
去查看vss(虚拟)和rss(实际)的部分。
虚拟内存空间一般会分为代码段,数据段,堆,栈等:
内存分段
程序执行时,函数中定义的各种变量,一般位于堆和栈中(上图中黄色、绿色、白色)部分。为了能动态分配内存,让程序运行过程中灵活使用,操作系统提供了很多相关函数,例如mmap/munmap,brk/sbrk,madvise,set_thread_area/get_thread_area等。C语言中malloc,free就是对这些基础函数的封装。
而其他高层语言中,例如Go,把这些对内存的操作完全屏蔽起来,写程序的过程中根本感受不到。
Go语言本身的实现中,内存由runtime自主管理。runtime与操作系统的交互并没有使用malloc这样的函数,而是通过汇编(或cgo)直接调用了mmap等函数。其内存分配核心思想非常类似于 TCMalloc ,不过由于部分特性(gc等)的需求,在TCMalloc的算法和设计上也做了部分修改。
基本概念
Go程序在启动的时候会向操作系统申请一块内存,然后分块管理。核心的结构是mheap, mcentral, mcache, mspan。
- mheap
全局,负责从os申请、释放内存。 - mcentral
全局,将内存按mspan划分,统一管理。访问需加锁。mcentral划分为_NumSizeClasses(目前是67)种mspan,每种又分为非空(nonempty,代表可以分配)和空(empty,可能是已满,可能是已分配给mcache在使用,代表不能分配)。非空和空分别形成双向链表,方便访问。 - mcache
每个P持有自己的mcache,于是获取内存的时候,无锁访问mcache。mcache也划分为_NumSizeClasses(目前是67)种mspan。
注:很多文章中,认为mcache中的每种mspan也是链表,但是我从代码中看来,好像mcache对每种mspan只会保存一个。这一点待更进一步理解。 - mspan
设计的精华,类似tcmalloc的机制。将一个或多个内存页形成一种mspan,一种mspan只负责分配固定size的内存(不足的时候,会向上补足,例如申请7byte,会使用8 byte类型的mspan)。mspan的列表见sizeclasses.go,里面详细记录了每一种mspan的分配规则,可存储object多少,浪费率等。
这种设计可以保证分配内存尽可能快,且能减少碎片的产生。
mspan, mcache 和 mcentral
相关代码
-
mallocinit
最初申请内存的地方在runtime/mallocinit(malloc.go)中。
func mallocinit() { if class_to_size[_TinySizeClass] != _TinySize { throw("bad TinySizeClass") } testdefersizes() if heapArenaBitmapBytes&(heapArenaBitmapBytes-1) != 0 { // heapBits expects modular arithmetic on bitmap // addresses to work. throw("heapArenaBitmapBytes not a power of 2") } // Copy class sizes out for statistics table. for i := range class_to_size { memstats.by_size[i].size = uint32(class_to_size[i]) }
初始化的过程相较于之前的版本,已经有了非常大的变化,不过核心也依然是作各种检查,然后初始化mheap(主要是初始化各种allocator,方便以后allocator真正分配内存。mcentral就是这个时候初始化的),尝试为当前的M(M是什么,参见调度的文章)分配mcache以及根据操作系统设置正确的arenaHints。
-
heapArena
之前的Go版本里,arena是大小512G的(网上很多图介绍,自行Google)。但我在go1.12源码里发现不是这样了。
mheap_.arenas
是一个二维数组[L1][L2]heapArena
(heapArena存的是对应arena的元数据),维度以及arena本身的大小和寻址bit位数相关,每个arena的起始地址按对应大小对齐。heapArena
这些元数据本身不存在heap里面。以我自己的机器(64位)为例,属于下图中的第一行。32位机器上是4M。计算方法是:
1 << 48 = 2 ^ 48 = 64M * 1 * 4M (即只有1行,4M列,每个大小64M)
arena大小
mheap_.arena
的这个二维数组中有些有对应的堆,有些没有(那么就是nil,例如垃圾回收之后,把内存还给了操作系统)。go的内存分配器总是尝试分配连续的arena,这样某些大的span可以跨越arena。
heapArena中bitmap用每2个bit记录一个指针大小(8byte)的内存信息,主要用于gc。spans是一个数组,长度等于一个heap中的页数(每页大小为8k,页数可能为64M/8k,不同架构会不同),每个页可能会指向一个span(即一个mspan指针)。实际上,对于分配的,空闲的和未分配的span,指针情况可能各不相同。pageInUse和pageMarks都是标记页的,按位处理。
-
mache初始化和fixalloc
再扯回来,第一次分配mcache,这里就涉及到真正的内存分配了。
allocmcache
中可以看到,之前已经初始化了cachealloc,这里调用alloc函数,走到的是fixalloc
的alloc函数:
func (f *fixalloc) alloc() unsafe.Pointer { if f.size == 0 { print("runtime: use of FixAlloc_Alloc before FixAlloc_Init\n") throw("runtime: internal error") } if f.list != nil { v := unsafe.Pointer(f.list) f.list = f.list.next f.inuse += f.size if f.zero { memclrNoHeapPointers(v, f.size) } return v } if uintptr(f.nchunk) < f.size { f.chunk = uintptr(persistentalloc(_FixAllocChunk, 0, f.stat)) f.nchunk = _FixAllocChunk } v := unsafe.Pointer(f.chunk) if f.first != nil { f.first(f.arg, v) } f.chunk = f.chunk + f.size f.nchunk -= uint32(f.size) f.inuse += f.size return v }
nchunk代表目前剩余的大小,size是目标大小。如果nchunk小于size,就从系统申请一块( _FixAllocChunk
这么大)内存,然后按照size这个固定的大小一点一点用。对于用完释放的,又会存在list属性中,供之后再次使用(使用的时候清零)。实际分配内存是通过persistentalloc一步步调用sysAlloc进而调用mmap分配的。
allocmcache
接下来就会把mcache中各个spanclass对应的mspan初始化为空mspan。
需要注意的是,上面这个特殊的M是这样初始化mcache。其实mcache是应该跟着P的,所以其他的mcache的初始化都是在procresize这个函数里,它在schedinit()中,位于mallocinit()之后。
-
newobject和mallocgc
上面讲了启动过程的各种初始化。初始化完毕,程序执行的时候,在堆上的对象是通过runtime.newobject函数来分配的。
什么时候分配在堆,什么时候分配在栈,这又是另外一个值得长篇探讨的问题,称为“逃逸分析”,这里暂不深入。
newobject的代码位于malloc.go中,它直接调用了mallocgc:
func newobject(typ *_type) unsafe.Pointer { return mallocgc(typ.size, typ, true) }
而mallocgc里面就是包含了分配内存时最核心的顺序和步骤,即小对象从mcache的freelist中开始分配,大对象(大于32k,即maxSmallSize)直接从堆上分配。小对象的分配又分为是否是tiny对象(以maxTinySize=16为界)。
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { ... size = maxTinySize } else { var sizeclass uint8 if size <= smallSizeMax-8 { sizeclass = size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv] } else { sizeclass = size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv] } size = uintptr(class_to_size[sizeclass]) spc := makeSpanClass(sizeclass, noscan) span := c.alloc[spc] v := nextFreeFast(span) if v == 0 { v, span, shouldhelpgc = c.nextFree(spc) } x = unsafe.Pointer(v) if needzero && span.needzero != 0 { memclrNoHeapPointers(unsafe.Pointer(v), size) } } } else { var s *mspan shouldhelpgc = true systemstack(func() { s = largeAlloc(size, needzero, noscan) }) s.freeindex = 1 s.allocCount = 1 x = unsafe.Pointer(s.base()) size = s.elemsize } var scanSize uintptr if !noscan { // If allocating a defer+arg block, now that we've picked a malloc size // large enough to hold everything, cut the "asked for" size down to // just the defer header, so that the GC bitmap will record the arg block // as containing nothing at all (as if it were unused space at the end of // a malloc block caused by size rounding). // The defer arg areas are scanned as part of scanstack. if typ == deferType { dataSize = unsafe.Sizeof(_defer{}) } heapBitsSetType(uintptr(x), size, dataSize, typ) if dataSize > typ.size { // Array allocation. If there are any // pointers, GC has to scan to the last // element. if typ.ptrdata != 0 { scanSize = dataSize - typ.size + typ.ptrdata } } else { scanSize = typ.ptrdata } c.local_scan += scanSize } ...
从核心分配代码看,先根据待分配对象size算出实际使用的sizeClass(即mspan的不同分类),然后算出spanClass,这里spanClass(本身是一个uint8)包含了span的size信息和span是否需要scan(用于gc)的信息,相当于mcache中alloc数组的index,数组是按一个noscan sizeClass一个scan sizeClass这样交替排列下去的。
算好这些, 就调用nextFreeFast尝试分配(mcache中),如果不成功,调用nextFree分配(还是mcache中),再不成功,调用memclrNoHeapPointers分配(mcentral或mheap中)。
nextFreeFast相对简单。mspan中allbits记录着哪些元素是已分配的,哪些未分配。alloccache用数字按位代表freeindex开始的
func nextFreeFast(s *mspan) gclinkptr { theBit := sys.Ctz64(s.allocCache) // Is there a free object in the allocCache? if theBit < 64 { result := s.freeindex + uintptr(theBit) if result < s.nelems { freeidx := result + 1 if freeidx%64 == 0 && freeidx != s.nelems { return 0 } s.allocCache >>= uint(theBit + 1) s.freeindex = freeidx s.allocCount++ return gclinkptr(result*s.elemsize + s.base()) } } return 0 }
但为了讲清这里的方式,需要简单了解mspan的内部结构,贴出一张网上盗的图,简单明了:
mspan内部主要结构
所以可以看到,mspan里面用位图记录了元素分配与否,直接查找即可。
nextFree稍微复杂,因为要处理分配不成功,继续向mcentral申请内存的逻辑:
func (c *mcache) nextFree(spc spanClass) (v gclinkptr, s *mspan, shouldhelpgc bool) { s = c.alloc[spc] shouldhelpgc = false freeIndex := s.nextFreeIndex() if freeIndex == s.nelems { // The span is full. if uintptr(s.allocCount) != s.nelems { println("runtime: s.allocCount=", s.allocCount, "s.nelems=", s.nelems) throw("s.allocCount != s.nelems && freeIndex == s.nelems") } c.refill(spc) shouldhelpgc = true s = c.alloc[spc] freeIndex = s.nextFreeIndex() } if freeIndex >= s.nelems { throw("freeIndex is not valid") } v = gclinkptr(freeIndex*s.elemsize + s.base()) s.allocCount++ if uintptr(s.allocCount) > s.nelems { println("s.allocCount=", s.allocCount, "s.nelems=", s.nelems) throw("s.allocCount > s.nelems") } return }
其中,refill函数会从mcentral甚至mheap获取mspan。
这块是最主要的分配逻辑。剩下的是tiny object(注意,需要不含指针且足够小)和large object的分配。它们做特殊处理都是因为太小或太大,而不适合适配到某些固定大小。在许多场景中tiny object这种分配策略能显著优化性能。
参考资料
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
How to Build a Billion Dollar App
George Berkowski / Little, Brown Book Group / 2015-4-1 / USD 24.95
Apps have changed the way we communicate, shop, play, interact and travel and their phenomenal popularity has presented possibly the biggest business opportunity in history. In How to Build a Billi......一起来看看 《How to Build a Billion Dollar App》 这本书的介绍吧!