本篇文章主要介绍golang的内存分配,文中关于内存分配的算法以及mcache的介绍均以实例展示,有需要的朋友可以参考一下。
程序内存大致可以分为5个段text
、data
、bss
、stack
、heap
其中text
段用于程序指令、文字、静态常量
data
与bss
段用于存储全局变量
stack
段用于存储函数调用与函数内的变量,stack
段的数据可以被CPU快速访问,stack
段的大小在运行时是不能增加和减少的,销毁只是通过栈指针的移动来实现的。同时,这也是为什么程序有时候会报错stack overflow的原因。
stack
段的内存分配是编译器实现的,我们无需关心。同时stack通常的大小是有限的。
因此对于大内存的分配,或者想手动创建或释放内存,就只能够对heap
段进行操作,这就是俗称的动态分配内存。例如c语言中的malloc
、calloc
、free
以及C++中的new
、delete
内存的分配属于操作系统级别的操作、因此不管是cc++语言的分配,最后都需要调用操作系统的接口。以linux为例,malloc代码可能调用了操作系统接口mmap
分配内存
linux操作系统提供的内存分配接口如下:
mmap/munmap 映射/释放 指定大小的内存.
brk/sbrk – 改变`data`段`结束的位置来扩展heap段的内存
madvise – 给操作系统建议如何管理内存
set_thread_area/get_thread_area – 操作线程本地存储空间
动态内存分配是操作系统为我们做的事情,其效率直接影响到运行在操作系统上的程序。对于一般的程序来说,例如c语言中实现的malloc
,最后都是通过调用操作系统的接口来实现的。
动态内存的调度是一个艰难复杂的话题,其要实现的目标包括:
内存分配的算法包括了:
K&R malloc
Region-based allocator
Buddy allocator
dlmalloc
slab allocator
TCMalloc(Thread-Caching Malloc)
TCMalloc是一种内存分配算法,比GNU C库中的malloc要快2倍,正如其名字一样,其是对于每一个线程构建了缓存内存。
TCMalloc解决了多线程时内存分配的锁竞争问题
TCMalloc对于小对象的分配非常高效
TCMalloc的核心思想是将内存划分为多个级别,以减少锁的粒度。在TCMalloc内部,内存管理分为两部分:小对象内存(thread memory)和大对象内存(page heap)。
小对象内存管理将内存页分成多个固定大小的可分配的free列表。因此,每个线程都会有一个无锁的小对象缓存,这使得在并行程序下分配小对象(<= 32k)非常有效。下图的对象代表的是字节。
分配小对象时
我们将在相同大小的线程本地free list中查找,如果有,则从列表中删除第一个对象并返回它
如果free list中为空,我们从中央free list中获取对象(中央free list由所有线程共享),将它们放在线程本地free list中,并返回其中一个对象
如果中央free list也为空,将从中央页分配器中分配内存页
,并将其分割为一组相同大小的对象,并将新对象放在中央free list中。和之前一样,将其中一些对象移动到线程本地空闲列表中
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
manualFreeList gclinkptr // list of free objects in mSpanManual spans
freeindex uintptr
nelems uintptr // number of object in the span.
allocCache uint64
allocBits *gcBits
gcmarkBits *gcBits
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 // number of allocated objects
spanclass spanClass // size class and noscan (uint8)
state mSpanStateBox // mSpanInUse etc; accessed atomically (get/set methods)
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
limit uintptr // end of data in span
speciallock mutex // guards specials list
specials *special // linked list of special records sorted by offset.
}
mcache
对于每一种大小规格都有两个类型:
scan -- 包含指针的对象。
noscan -- 不包含指针的对象。
mcentral
mheap
central [numSpanClasses]struct {
mcentral mcentral
pad [unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byte
}
由于我们有各个规格的 span 的 mcentral,当一个 mcache 从 mcentral 申请 mspan 时,只需要在独立的 mcentral 级别中使用锁,所以其它任何 mcache 在同一时间申请不同大小规格的 mspan 将互不受影响可以正常申请。
pad为格外增加的字节。对齐填充(Pad)用于确保 mcentrals 以 CacheLineSize 个字节数分隔,所以每一个 MCentral.lock 都可以获取自己的缓存行(cache line),以避免伪共享(false sharing)问题。
图中对应的free[_MaxMHeapList]mSpanList
:一个 spanList 数组。每一个 spanList 中的 mspan 包含 1 ~ 127(_MaxMHeapList - 1)个页。例如,free[3] 是一个包含 3 个页的 mspan 链表。free 表示 free list,表示未分配。对应 busy list。
freelarge mSpanList:一个 mspan 的列表,每一个元素(mspan)的页数大于 127,通过 mtreap 结构体管理。busylarge与之相对应。
在进行内存分配时,go按照大小分成3种对象类
小于16个字节的对象Tiny类
适用于最大32 kB的Small类
适用于大对象的large类
Small类会被分为大约有70个大小,每一个大小都拥有一个free list
引入Tiny这一微小对象是为了适应小字符串和独立的转义变量。
Tiny微小对象将几个微小的分配请求组合到一个16字节的内存块中
当分配Tiny对象时:
small对象分配与Tiny对象类似,
分配和释放大对象直接使用mheap
,就像在TCMalloc中一样,管理了一组free list
大对象被四舍五入为页大小(8K)的倍数,在free list中查找第k个free list,如果其为空,则继续查找更大的一个free list,直到第128个free list
如果在第127个free list中找不到,我们在剩余的大内存页(mspan.freelarge
字段)中查找跨度,如果失败,则从操作系统获取。
关于golang的内存分配就分享到这里了,希望以上内容可以对大家有一定的帮助,可以学到更多知识。如果喜欢这篇文章,不如把它分享出去让更多的人看到。