分享
  1. 首页
  2. 文章

Go 内存管理

nino's blog · · 2184 次点击 · · 开始浏览
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

内存管理缓存结构

Go实现的内存管理采用了tcmalloc这种架构,并配合goroutine和垃圾回收。tcmalloc的基本策略就是将内存分为多个级别。申请对象优先从最小级别的内存管理集合mcache中获取,若mcache无法命中则需要向mcentral申请一批内存块缓存到本地mcache中,若mcentral无空闲的内存块,则向mheap申请来填充mcentral,最后向系统申请。

mcache + mspan

最小级别的内存块管理集合mcache由goroutine自己维护,这样从中申请内存不用加锁。它是一个大小为67的数组,不同的index对应不同规格的mspannewobject的时候通过sizetoclass计算对应的规格,然后在mcache中获取mspan对象。


type mcache struct {
 alloc [_NumSizeClasses]*mspan // spans to allocate from
}

mspan包含着一批大小相同的空闲的object,由freelist指针查找。mspan内部的object是连续内存块,即连续的n个page(4KB)的连续内存空间。然后这块空间被平均分成了规格相同的object,这些object又连接成链表。当newobject时找到mcache中对应规格的mspan,从它的freelist取一个object即可。


type mspan struct {
 next *mspan // in a span linked list
 prev *mspan // in a span linked list
 start pageID // starting page number
 npages uintptr // number of pages in span
 freelist gclinkptr // list of free objects
 sizeclass uint8 // size class
 incache bool // being used by an mcache
}

mheap + mcentral

如果某个规格的span里已经没有freeObject了 需要从mcentral当中获取这种规格的mspan。正好mcentral也是按照class规格存储在数组中,只要按规格去mheap的mcentral数组取mspan就好。


// 某种规格的mspan正好对应一个mcentral
type mcentral struct {
 lock mutex
 sizeclass int32
 nonempty mspan //还有空闲object的mspan
 empty mspan //没有空闲object或已被cache取走的mspan
}

如果central数组中这种规格的mcentral没有freeSpan了,则需要从mheapfree数组获取。这里规格并不对齐,所以应该要重新切分成相应规格的mspan。


type mheap struct {
 lock mutex
 free [_MaxMHeapList]mspan // 页数在127以内的空闲span链表
 freelarge mspan 
 spans **mspan
 
 bitmap uintptr
 bitmap_mapped uintptr
 arena_start uintptr
 arena_used uintptr 
 arena_end uintptr
 arena_reserved bool
 central [_NumSizeClasses]struct {
 mcentral mcentral
 pad [_CacheLineSize]byte
 }
 spanalloc fixalloc // allocator for span*
 cachealloc fixalloc // allocator for mcache*
}

内存的初始化

很早之前看过这个图,当时对他的理解有误,因为看漏了一句话 struct Mcache alloc from 'cachealloc' by FixAlloc。就是说用户进程newobject是从下图的arena区域分配的,而runtime层自身管理的结构 比如mcache等是专门设计了fixAlloc来分配的,原因可能是这些runtime层的管理对象类型和长度都相对固定,而且生命周期很长,不适合占用arena区域。

mallocinit

通过sysReserve 向系统申请一块连续的内存 spans+bitmap+arena。其中arena为各个级别缓存结构提供的分配的内存块,spans是个指针数组用来按照page寻址arena区域。

最终sysReserve调用的是系统调用mmap。申请了512GB的虚拟地址空间,真正的物理内存则是用到的时候发生缺页才真实占用的。


func mallocinit() {
 // 初始化规格class和size的对照方法
 initSizes()
 if ptrSize == 8 && (limit == 0 || limit> 1<<30) { arenaSize := round(_MaxMem, _PageSize) bitmapSize = arenaSize / (ptrSize * 8 / 4) spansSize = arenaSize / _PageSize * ptrSize pSize = bitmapSize + spansSize + arenaSize + _PageSize p1 = uintptr(sysReserve(unsafe.Pointer(p), pSize, &reserved)) } mheap_.spans = (**mspan)(unsafe.Pointer(p1)) mheap_.bitmap = p1 + spansSize mheap_.arena_start = p1 + (spansSize + bitmapSize) mheap_.arena_used = mheap_.arena_start mheap_.arena_end = p + pSize mheap_.arena_reserved = reserved mHeap_Init(&mheap_, spansSize) _g_ := getg() _g_.m.mcache = allocmcache() } 

mheap初始化相关指针,使之可以寻址arena这块内存。同时初始化cachealloc这个固定分配器。最后执行的 m.mcache = allocmcache() 是每个gouroutine创建时都要初始化的。直到这时才真正创建了mcache,并且初始化mcache里整个数组对应的mspan为emptyspan。


func (h *mheap) init(spansStart, spansBytes uintptr) {
 h.spanalloc.init(unsafe.Sizeof(mspan{}), recordspan, unsafe.Pointer(h), &memstats.mspan_sys)
 h.cachealloc.init(unsafe.Sizeof(mcache{}), nil, nil, &memstats.mcache_sys)
 h.spanalloc.zero = false
 for i := range h.free {
 h.free[i].init()
 h.busy[i].init()
 }
 h.freelarge.init()
 h.busylarge.init()
 for i := range h.central {
 h.central[i].mcentral.init(int32(i))
 }
 sp := (*slice)(unsafe.Pointer(&h.spans))
 sp.array = unsafe.Pointer(spansStart)
 sp.len = 0
 sp.cap = int(spansBytes / sys.PtrSize)
}
func allocmcache() *mcache {
 // lock and fixalloc mcache
 c := (*mcache)(mheap_.cachealloc.alloc())
 for i := 0; i < _NumSizeClasses; i++ { c.alloc[i] = &emptymspan } return c } 

fixalloc

fixalloc分配器通过init初始化每次分配的size。chunk是每次分配的固定大小的内存块,list是内存块链表。当fixalloc初始化为cachealloc时,每次调用alloc就分配一块mcache。persistantalloc看起来是runtime有个全局存储的后备内存的地方,优先从这儿取没有再从系统mmap一块。


type fixalloc struct {
 size uintptr
 first func(arg, p unsafe.Pointer)
 arg unsafe.Pointer
 list *mlink
 chunk unsafe.Pointer
 nchunk uint32
 inuse uintptr // in-use bytes now
 stat *uint64
 zero bool // zero allocations
}
func (f *fixalloc) alloc() unsafe.Pointer {
 // 优先从可复用链表中获取对象块
 if f.list != nil {
 f.list = f.list.next
 return v
 }
 // 如果没有从系统申请chunk大小的内存块
 if uintptr(f.nchunk) < f.size { f.chunk = persistentalloc(_FixAllocChunk, 0, f.stat) } v := f.chunk // 为调用方提供了fist函数作为hook点 return v } 

内存分配

mallocgc

以下总结了malloc的流程,基本普通的小对象都是从mcache中找到相应规格的mspan,在其中的freelist上拿到object对象内存块。nextfree中隐藏了整个内存数据块的查找和流向。


func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
 c := gomcache()
 if size <= maxSmallSize { // size小于16bit的不用扫描的对象 直接从mcache的tiny上分 if noscan && size < maxTinySize { off := c.tinyoffset if off+size <= maxTinySize && c.tiny != 0 { x = unsafe.Pointer(c.tiny + off) return x } // 若没有tiny了则从mcache的中相应规格的mspan查找 span := c.alloc[tinySizeClass] v, _, shouldhelpgc = c.nextFree(tinySizeClass) x = unsafe.Pointer(v) } else { // 普通小于4KB小对象先计算规格 span := c.alloc[sizeclass] v, span, shouldhelpgc = c.nextFree(sizeclass) } } else { // 大对象直接从heap分配span systemstack(func() { s = largeAlloc(size, needzero) }) x = unsafe.Pointer(s.base()) } return x } func (c *mcache) nextFree(sizeclass uint8) (v gclinkptr, s *mspan, shouldhelpgc bool) { s = c.alloc[sizeclass] freeIndex := s.nextFreeIndex() if freeIndex == s.nelems { systemstack(func() { c.refill(int32(sizeclass)) }) s = c.alloc[sizeclass] freeIndex = s.nextFreeIndex() } v = gclinkptr(freeIndex*s.elemsize + s.base()) return } 

refill + cachespan

如果nextfree在mcache相应规格的mspan里拿不到object那么需要从mcentral中refill内存块。

这里面有个细节要将alloc中原本已经没有可用object的这块mspan还给central,应该要放进central的empty链表中。这里只是把相应的mspan的incache设置为false,等待sweep的回收。


func (c *mcache) refill(sizeclass int32) *mspan {
 s := c.alloc[sizeclass]
 if s != &emptymspan {
 s.incache = false
 }
 s = mheap_.central[sizeclass].mcentral.cacheSpan()
 c.alloc[sizeclass] = s
 return s
}

sweepgen是个回收标记,当sweepgen=sg-2时表示等待回收,sweepgen-1表示正在回收,sweepgen表示已经回收。从mcentral中获取mspan时有可能当前的span正在等待或正在回收,我们把等待回收的mspan可以返回用来refill mcache,因此将它insert到empty链表中。


func (c *mcentral) cacheSpan() *mspan {
 sg := mheap_.sweepgen
retry:
 var s *mspan
 for s = c.nonempty.first; s != nil; s = s.next {
 if s.sweepgen == sg-2 && atomic.Cas(&s.sweepgen, sg-2, sg-1) {
 // 等待回收 可以返回使用
 c.nonempty.remove(s)
 c.empty.insertBack(s)
 s.sweep(true)
 goto havespan
 }
 if s.sweepgen == sg-1 {
 // 正在回收 忽略
 continue
 }
 c.nonempty.remove(s)
 c.empty.insertBack(s)
 goto havespan
 }
 for s = c.empty.first; s != nil; s = s.next {...}
 s = c.grow()
 c.empty.insertBack(s)
havespan:
 ...
 return s
}

mcentral grow

如果mcentral中没有mspan可以用 那么需要grow,即从mheap中获取。要计算出当前规格对应的page数目,从mheap中直接去nPage的mspan。free区域是个指针数组,每个指针对应一个mspan的链表,数组按照npage寻址。若大于要求的npage的链表中 都没有空闲mspan,则mheap也需要扩张。


func (c *mcentral) grow() *mspan {
 npages := uintptr(class_to_allocnpages[c.sizeclass])
 size := uintptr(class_to_size[c.sizeclass])
 n := (npages << _PageShift) / size s := mheap_.alloc(npages, c.sizeclass, false, true) heapBitsForSpan(s.base()).initSpan(s) return s } func (h *mheap) allocSpanLocked(npage uintptr) *mspan { for i := int(npage); i < len(h.free); i++ { list = &h.free[i] if !list.isEmpty() { s = list.first goto HaveSpan } } list = &h.freelarge s = h.allocLarge(npage) if s == nil { if !h.grow(npage) { return nil } s = h.allocLarge(npage) } HaveSpan: // Mark span in use. return s } 

mheap grow

mheap的扩张h.sysAlloc直接向arena区域申请nbytes的内存,数目按照npage大小计算。arena区域的一些指针标记开始移动,最终将mspan加入链表,等待分配。


func (h *mheap) grow(npage uintptr) bool {
 ask := npage << _PageShift v := h.sysAlloc(ask) s := (*mspan)(h.spanalloc.alloc()) s.init(uintptr(v), ask>>_PageShift)
 p := (s.base() - h.arena_start)>> _PageShift
 for i := p; i < p+s.npages; i++ { h.spans[i] = s } atomic.Store(&s.sweepgen, h.sweepgen) s.state = _MSpanInUse h.pagesInUse += uint64(s.npages) // 加入链表 h.freeSpanLocked(s, false, true, 0) return true } 

内存回收与释放

简单说两句:mspan里有sweepgen回收标记,回收的内存会先全部回到mcentral。如果已经回收所有的mspan那么可以返还给mheap的freelist。回收的内存块当然是为了复用,并不直接释放。


func (s *mspan) sweep(preserve bool) bool {
 res = mheap_.central[cl].mcentral.freeSpan(s, preserve, wasempty)
}
func (c *mcentral) freeSpan(s *mspan, preserve bool, 
wasempty bool) bool {
 if wasempty {
 c.empty.remove(s)
 c.nonempty.insert(s)
 }
 ...
 c.nonempty.remove(s)
 mheap_.freeSpan(s, 0)
 return true
}

监控线程sysmon又出现了,它会遍历mheap中所有的free freelarge里的mspan,发现空闲时间超过阈值就madvise建议内核释放它相关的物理内存。


有疑问加站长微信联系(非本文作者)

本文来自:nino's blog

感谢作者:nino's blog

查看原文:Go 内存管理

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

关注微信
2184 次点击
被以下专栏收入,发现更多相似内容
添加一条新回复 (您需要 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传

用户登录

没有账号?注册
(追記) (追記ここまで)

今日阅读排行

    加载中
(追記) (追記ここまで)

一周阅读排行

    加载中

关注我

  • 扫码关注领全套学习资料 关注微信公众号
  • 加入 QQ 群:
    • 192706294(已满)
    • 731990104(已满)
    • 798786647(已满)
    • 729884609(已满)
    • 977810755(已满)
    • 815126783(已满)
    • 812540095(已满)
    • 1006366459(已满)
    • 692541889

  • 关注微信公众号
  • 加入微信群:liuxiaoyan-s,备注入群
  • 也欢迎加入知识星球 Go粉丝们(免费)

给该专栏投稿 写篇新文章

每篇文章有总共有 5 次投稿机会

收入到我管理的专栏 新建专栏