Go 内存管理

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

内存管理缓存结构

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

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