golang内存分配分析
Table of Contents
1 前言
资源管理感觉一直是一个难点,而其中的大头又是内存管理。写c跟写c++的时候的内存泄露简直是个灾难。所以之后的很多语言都自带gc。但是gc也只能管理内存资源,其他的如数据库连接,文件描述符等还是需要程序员自己管理,go里面使用defer,提供一丢丢的好处,但是这个方案依旧十分粗糙,不过新出来的rust语言对这个做的比较好。在变量的生命周期失效的时候会回收变量所拥有的资源。内存管理分为内存分配跟内存回收两大类,这篇是看go内存分配的源码笔记。
2 mspan结构
mspan是一个对系统内存的一个封装。结构如下:
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 // For debugging. TODO: Remove. startAddr uintptr // address of first byte of span aka s.base() npages uintptr // number of pages in span stackfreelist gclinkptr // list of free stacks, avoids overloading freelist freeindex uintptr nelems uintptr // number of object in the span. allocCache uint64 allocBits *uint8 gcmarkBits *uint8 sweepgen uint32 divMul uint16 // for divide by elemsize - divMagic.mul baseMask uint16 // if non-0, elemsize is a power of 2, & this will get object allocation base allocCount uint16 // capacity - number of objects in freelist sizeclass uint8 // size class incache bool // being used by an mcache state mSpanState // mspaninuse etc needzero uint8 // needs to be zeroed before allocation divShift uint8 // for divide by elemsize - divMagic.shift divShift2 uint8 // for divide by elemsize - divMagic.shift2 elemsize uintptr // computed from sizeclass or from npages unusedsince int64 // first time spotted by gc in mspanfree state npreleased uintptr // number of pages released to the os limit uintptr // end of data in span speciallock mutex // guards specials list specials *special // linked list of special records sorted by offset. }
mspan这个结构体本身是从fixalloc(mheap.spanalloc)中分配的,而其中包裹的内存是mheap分配的。其中startAddr就是其包裹内存的起始地址. 每次mspan被分配都会调用mheap.go中的recordspan函数,这个函数主要是把分配的mspan记录到mheap.allspans中 mspan有4种状态:
- _MSpanDead 未使用未清理
- _MSpanInUse 在堆中使用
- _MSpanStack 在栈中使用
- _MSpanFree 未使用
3 fixalloc
fixalloc是一个固定大小的分配器。主要用来分配一些对内存的包装的结构,比如:mspan,mcache..等等,虽然启动分配的实际使用内存是由其他内存分配器分配的。 主要分配思路为: 开始的时候一次性分配一大块内存,每次请求分配一小块,释放时放在list链表中,由于size是不变的,所以不会出现内存碎片。
type fixalloc struct { size uintptr //初始化函数和参数 first func(arg, p unsafe.Pointer) // called first time p is returned arg unsafe.Pointer list *mlink //一个通用的链表 chunk unsafe.Pointer //一大块内存 nchunk uint32 inuse uintptr // in-use bytes now 记录在使用中的bytes stat *uint64 zero bool // zero allocations }
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 { //如果list不为空,直接拿 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 = persistentalloc(_FixAllocChunk, 0, f.stat) f.nchunk = _FixAllocChunk } //从chunk中分配一个固定大小的size,当释放的时候,会回归到list中 v := f.chunk if f.first != nil { f.first(f.arg, v) } f.chunk = add(f.chunk, f.size) f.nchunk -= uint32(f.size) f.inuse += f.size return v } //释放时直接放回list中 func (f *fixalloc) free(p unsafe.Pointer) { f.inuse -= f.size v := (*mlink)(p) v.next = f.list f.list = v }
4 mcache
mcache是每一个p一个mcache,所以不会有多个核并发访问,所以不用加锁,分配释放速度非常快。主要用于小对象的分配 分配思路:
- 如果size小于maxTinySize(16byte),这从mcache中的tiny中分配数据。如果tiny不够用,则分配一个新的tiny块(tiny分配器重要是把小对象放在一起)
- 如果size小于maxSmallSize大于maxTinySize, 则从mcache中的alloc中分配
type mcache struct { next_sample int32 // trigger heap sample after allocating this many bytes local_scan uintptr // bytes of scannable heap allocated tiny uintptr //小对象的分配, 也是一大块内存空间 tinyoffset uintptr local_tinyallocs uintptr // number of tiny allocs not counted in other stats alloc [_NumSizeClasses]*mspan // spans to allocate from 主要的分配地址 stackcache [_NumStackOrders]stackfreelist local_nlookup uintptr // number of pointer lookups local_largefree uintptr // bytes freed for large objects (>maxsmallsize) local_nlargefree uintptr // number of frees for large objects (>maxsmallsize) local_nsmallfree [_NumSizeClasses]uintptr // number of frees for small objects (<=maxsmallsize) }
mcache通过nextFree函数进行填充mcache
func (c *mcache) nextFree(sizeclass uint8) (v gclinkptr, s *mspan, shouldhelpgc bool) { s = c.alloc[sizeclass] 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") } //调用refill填充mcache systemstack(func() { c.refill(int32(sizeclass)) }) shouldhelpgc = true s = c.alloc[sizeclass] 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函数会从mheap中的central中拿取相应大小的空间,每一个大小的central都维护了一个mcentral,首先从mcentral的nonempty中找到一个空闲对象。如果没有的话就从empty对象中找到一个空闲对象。如果都没有,就调用grow函数,它会通过mheap的alloc函数直接分配
5 mcentral
在mcache refill的时候会调用mcentral分配器分配内存, 主要分配思路如下
- 从nonempty找到一个可以使用的mspan,清理下然后返回
- 从empty找到一个mspan,直接返回
- 如果都没有,调用grow扩展内存
grow函数:
- 使用mheap.进行分配mspan
- 分配处理的mspan会插入到empty列表
6 mheap
分配span过程:
- 从mheap.free中分配(free是一个数组链)
- 如果没有从mheap.freelarge中分配(freelarge是一个链)
- 如果freelarge没有会调用h.grow使用h.sysAlloc进行系统调用(会调用sysReverve函数,linux下使用mmap)
- h.grow申请了一次内存,会使用再释放,这是为了让这次分配的内存与以前的合并
- h.spans记录了此spans是谁使用
large分配:
- large使用的是freelarge字段,这是一个mSpanList,使用bestfit算法找到满足要求的最小地址最低的内存块.
7 分配一个对象流程
package main import ( "fmt" ) type TTT struct { Name string Age int64 } func main() { a := new(TTT) a.Name = "dddddd" fmt.Println(a) }
使用`go build -gcflags '-N -l' malloc.go` 禁止编译器优化,然后使用`go tool objdump -s "main.main" ./malloc`查看main.main函数的代码,可以看到以下汇编指令:
malloc.go:13 0x1087101 488d05f86f0100 LEAQ 0x16ff8(IP), AX malloc.go:13 0x1087108 48890424 MOVQ AX, 0(SP) malloc.go:13 0x108710c e8bf6cf8ff CALL runtime.newobject(SB) malloc.go:13 0x1087111 488b442408 MOVQ 0x8(SP), AX
从这里可以看到分配对象调用了runtime包中国的newobject函数, 以下go源码中也说明了这点newobject正是new内建指令对应的操作
// implementation of new builtin // compiler (both frontend and SSA backend) knows the signature // of this function func newobject(typ *_type) unsafe.Pointer { return mallocgc(typ.size, typ, true) }
mallocgc实现中,小对象从每个p结构的cache中分配(就是mcache),大对象直接从堆中分配。mallocgc步骤如下:
- 如果size小于maxTinySize(16byte),这从mcache中的tiny中分配数据。如果tiny不够用,则分配一个新的tiny块(tiny分配器重要是把小对象放在一起)
- 如果size小于maxSmallSize大于maxTinySize, 则从mcache中的alloc中分配
- 如果size大于maxSmallSize则调用largeAlloc函数进行分配
- largeAlloc会从mheap中分配