jasper的技术小窝

关注DevOps、运维监控、Python、Golang、开源、大数据、web开发、互联网

深入Golang之内存管理

作者:jasper | 分类:Golang | 标签:   | 阅读 1004 次 | 发布:2017-12-17 9:20 p.m.

我们知道Golang之所以比C开发起来容易的一个最主要原因就是它有自己的内存管理,有自己的GC机制。这让我们不用自己去关心对内存作管理。但是你使用这一门语言的时候,又必须知道它底层对于内存的管理,这样对于优化编程和排障等都有好处。

tcmalloc

在介绍Golang的内存管理之前,我们先来介绍tcmalloc的实现,因为它们的原理比较像。

tcmalloc是Google推出的一种内存分配器,其原理是对待分配的内存分为全局缓存和进程的私有缓存。对于小容量的内存直接从私有缓存申请,私有的不足了才会向全局缓存申请;对于大容量的内存则直接从全局缓存申请;其边界是32k。tcmalloc使用span来管理内存分页。

数据结构

在介绍内存管理的细节之前,先来看几个和内存分配管理有关的数据结构:

mcache

type mcache struct {
    next_sample int32  
    local_scan  uintptr 
    tiny             uintptr
    tinyoffset       uintptr
    local_tinyallocs uintptr 

    alloc [numSpanClasses]*mspan 
    stackcache [_NumStackOrders]stackfreelist

    local_nlookup    uintptr                
    local_largefree  uintptr                    local_nlargefree uintptr                
    local_nsmallfree [_NumSizeClasses]uintptr 
}

mcache是绑定在每个P上面的内存,主要用于小对象,正因为是每个P私有的,所以分配的时候就不用加锁。其中小对象都是通过tiny来分配的,再来看alloc [numSpanClasses]*mspan这是一个大小为67的指针数组,数组里面的元素是mspan的指针,同tcmalloc中的span一样,这里的mspan中记录了需要分配的块的大小。

mspan

type mspan struct {
    next *mspan    
    prev *mspan    
    list *mSpanList 

    startAddr uintptr
    npages    uintptr // span中的页的数量

    manualFreeList gclinkptr

    freeindex uintptr
    nelems uintptr // span中块的总数目

    allocCache uint64 
    state       mSpanState // span有四种状态:_MSpanDead,_MSpanInUse,_MSpanManual,_MSpanFree
    elemsize    uintptr // 通过spanClass或者npages算出来

mspan作为内存管理的基本单位而存在,其数据结构为若干连续内存页,一个双端链表的形式,里面存储了它的一些位置信息。通过一个基地址+(页号*页大小),就可以定位到这个mspan的实际内存空间。

mcentral

如果P上面的mcache不够用的时候,就会向mcentral来申请,来看看mcentral的数据结构:

type mcentral struct {
    lock      mutex
    spanclass spanClass
    nonempty  mSpanList 
    empty     mSpanList 

    nmalloc uint64
}

type mSpanList struct {
    first *mspan 
    last  *mspan 
}

其中nonempty是mspan的双向链表,表示当前mcentral中可用的mspan list;而empty是已经被用了的mspan list,或者是在mcache里面已经被缓存了。注意这里有一个lock,因为不同于mcache,mcentral是全局的,会存在多个P访问mcentral的情况,所以这里的lock是非常有必要的。

mheap

如果mcentral中的内存不够用的时候呢,这时候会继续向mheap来申请:

type mheap struct {
    lock      mutex
    free      [_MaxMHeapList]mSpanList // 长度为_MaxMHeapList的没有用的MSpanList
    freelarge mTreap                   // 长度大于_MaxMHeapList的没有用的mTreap

    busy      [_MaxMHeapList]mSpanList // 被使用的large span的列表
    busylarge mSpanList                // 长度大于_MaxMHeapList的被使用的large span列表

    allspans []*mspan // 所有的mspan

    spans []*mspan // 记录arena区域页号(page number)和mspan的映射关系

    // bitmap是spans的标志位
    bitmap        uintptr 
    bitmap_mapped uintptr

    // arena是堆生成区
    arena_start uintptr
    arena_used  uintptr 
    arena_alloc uintptr
    arena_end   uintptr

    // 之前介绍的 mcentral,每种大小的块对应一个 mcentral
    central [numSpanClasses]struct {
        mcentral mcentral
        pad      [sys.CacheLineSize - unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byte
    }

    // 用来分配特定大小的块
    spanalloc             fixalloc                  
    cachealloc            fixalloc
    treapalloc            fixalloc 
    specialfinalizeralloc fixalloc 
    specialprofilealloc   fixalloc

在初始化的时候,mheap会被初始化一个全局变量mheap_。可以看到其内存布局为:

+--------------+----------+-------------------------+
| spans .......| bitmap | arena ..................|
+--------------+----------+-------------------------+

arena是Golang中用于分配内存的连续虚拟地址区域。堆上申请的所有内存都来自arena。操作系统常见有两种做法标志内存可用:一种是用链表将所有的可用内存都串起来;另一种是使用位图来标志内存块是否可用。

盗图来直观地看一下:

在了解了几个主要的数据结构之后,下面我们来看下分配过程。

内存分配

当我们调用new来创建一个对象的时候,其实调用的是newobject函数:

func newobject(typ *_type) unsafe.Pointer {
    return mallocgc(typ.size, typ, true)
}

直接调用mallocgc函数,三个参数分别表示对象类型大小,对象的类型,以及一个标志位代表GC是否需要扫描这个对象,默认是需要的。下面我们分步来看这个mallocgc:

mp := acquirem()
mp.mallocing = 1
shouldhelpgc := false
dataSize := size
c := gomcache()

首先获取当前goroutine的m,然后锁住当前的m进行分配,拿到当前goroutine的mache之后,根据size来判断是large对象,small对象还是tiny对象。

1、 如果是tiny对象,则直接从mache里面分配:

off := c.tinyoffset

// 地址对齐
if size&7 == 0 {
    off = round(off, 8)
} else if size&3 == 0 {
    off = round(off, 4)
} else if size&1 == 0 {
    off = round(off, 2)
}

// 如果tiny足够的话,就直接分配
if off+size <= maxTinySize && c.tiny != 0 {
    x = unsafe.Pointer(c.tiny + off)
    c.tinyoffset = off + size
    c.local_tinyallocs++
    mp.mallocing = 0
    releasem(mp)
    return x
}

// 不然的话,为其重新分配一个16byte内存块
span := c.alloc[tinySpanClass]

tinyoffset表示tiny分配到什么地址,之后的分配根据 tinyoffset寻址。先根据要分配的对象大小进行地址对齐,比如size是8的倍数,tinyoffset和8对齐。然后就是分配,如果tiny剩余的空间不够,则重新申请一个16byte的内存块,并分配给object。如果有结余,则记录在tiny上。

2、 如果是small对象:

var sizeclass uint8
// 计算需要分配的sizeclass
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)

// 获取mcache中预先分配的spans链表
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)
}

先计算应该分配的sizeclass,然后去mcache里面alloc申请,如果mcache.alloc[spc]申请不到,则mcache向mcentral申请,然后再分配,这部分的直接拿出最后申请部分的代码:

s = mheap_.central[spc].mcentral.cacheSpan()
c.alloc[spc] = s

其中在mcentral.cacheSpan()里面,如果发现mcentral里面都不够的话,就会像mheap申请:

npages := uintptr(class_to_allocnpages[c.spanclass.sizeclass()])
size := uintptr(class_to_size[c.spanclass.sizeclass()])
n := (npages << _PageShift) / size

// 向mheap申请
s := mheap_.alloc(npages, c.spanclass, false, true)
if s == nil {
    return nil
}

p := s.base()
s.limit = p + size*n

heapBitsForSpan(s.base()).initSpan(s)
return s

哈哈,如果mheap里面仍然不够呢,就会向OS去申请内存了:

p := uintptr(sysReserve(unsafe.Pointer(h.arena_end), p_size, &reserved))

不同的操作系统有不同的sysReserve实现,其中在linux中的实现是用mmap来向操作系统内核申请新的虚拟地址区间:

func mmap(addr unsafe.Pointer, n uintptr, prot, flags, fd int32, off uint32) unsafe.Pointer {
    if _cgo_mmap != nil {
        var ret uintptr
        systemstack(func() {
            ret = callCgoMmap(addr, n, prot, flags, fd, off)
        })
        return unsafe.Pointer(ret)
    }
    return sysMmap(addr, n, prot, flags, fd, off)
}

底层是调用CGO的Mmap方法,我们就不再继续往下深究了。

3、 如果是large对象,就直接调用largeAlloc向mheap申请了:

    s := mheap_.alloc(npages, makeSpanClass(0, noscan), true, needzero)

至于内存mheap不够的时候处理办法和上面的2中一样。

那么整个的分配过程大体就是这些了,与之相对应的,还有一块就是内存的回收,这就牵扯到Golang的GC机制了,这个等下一篇单独介绍。

总结

简单总结一下Golang内存分配的逻辑吧:

  • object size>32KB, 则直接使用mheap来分配空间;
  • object size<16Byte, 则通过mcache的tiny分配器来分配;
  • object size在上面两者之间,首先尝试通过sizeclass对应的分配器分配;
  • 如果mcache没有空闲的span,则向mcentral申请空闲块;
  • 如果mcentral也没空闲块,则向mheap申请并进行切分;
  • 如果mheap也没合适的span,则向操作系统申请。

总的来说,就是实现了tcmalloc的理论。好,那我们下一篇再来谈谈Golang的GC。


转载请注明出处:http://www.opscoder.info/golang_mem_management.html

【上一篇】 深入Golang之http
【下一篇】 深入Golang之垃圾回收
其他分类: