Go的内存管理

预备知识:
内存分成5个区,他们分别是堆,栈,文件映射段,数据段,代码段
1 堆,程序运行过程中动态,分配任意大小的内存。频繁的分配和释放必然会产生碎片。
2 栈 包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是8MB,由编译器在需要时自动分配和释放。
3 数据段,包括全局变量,静态变量的空间等
4 代码段,包括代码和常量等。也可以叫做只读段
5 文件映射段,包括动态库、共享内存等,从高地址开始向下增长。一般是mmap函数所分配的虚拟地址空间
Go在程序启动的时候,会先向操作系统申请一块内存(注意这时还只是一段虚拟的地址空间,并不会真正地分配内存),切成小块后自己进行管理。
申请到的内存块被分配了三个区域,在64位机器上分别是512MB,16GB,512GB大小。
说明:
arena区域就是我们所谓的堆区,Go动态分配的内存都是在这个区域,它把内存分割成8KB大小的页,一些页组合起来称为mspan。其中arena 中文有舞台,竞技场的意思。
bitmap区域标识arena哪些地址保存了对象,并且用2bit标志位表示对象是否包含指针、GC标记信息。bitmap中1个byte大小的内存对应arena区域中4个指针大小(指针大小为8B)的内存,所以bitmap区域的大小512G/(4*8B) = 16GB;
spans区域存放mspan的指针,每个指针对应一页,所以spans区域的大小就是512GB/8KB*8B=512MB,除以8KB是计算arena区域的页数。一页大小在8KB,每个指针大小是8byte;创建mspan的时候,按页填充对应的spans区域,在回收object时,根据地址很容易就能找到它所属的mspan
内存管理单元:
mspan:Go中内存管理的基本单元,是由一片连续的8KB的页组成的大块内存;注意,这里的页和操作系统本身的页并不是一回事,它一般是操作系统页大小的几倍;(系统操作页一般是4KB)。mspan是一个包含起始地址、mspan规格、页的数量等内容的双端链表。
每个mspan按照它自身的属性Size Class的大小分割成若干个object,每个object可存储一个对象。并且会使用一个位图(bitmap)来标记其尚未使用的object。
属性Size Class决定object大小,而mspan只会分配给和object尺寸大小接近的对象,当然,对象的大小要小于object大小。
const _NumSizeClasses = 67
var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}
这里mspan的属性Size class大小是class_to_size 整型数组的索引下标,对应的里面的元素大小才是真正分配object的大小。比如说某mspan的Size class 等于3,那么object大小就是32B,32B大小的object可以存储对象大小范围在17B~32B的对象。而对于微小对象(小于16B),分配器会将其进行合并,将几个对象分配到同一个object中。数组里最大的数是32768,也就是32KB,超过此大小就是大对象了,它会被特别对待,类型Size Class为0表示大对象,它实际上直接由堆内存分配,而小对象都要通过mspan来分配。
小节总结:
1 go启动把申请到的内存分三段arena区域,bitmap区域,spans。
2 arena就是所谓的堆,它把内存分割为8kb的页,页组合起来成为mspan。
3 bitmap区域,标识arena的哪些地址存了对象。
4 spans区域存放mspan的指针,每个指针对应一页。
5 mspan 是go中内存管理的基本单元,由连续8kb的页组成的大块内存。它的数据结构是双端列表。
下面是mspan的结构体定义:
//go:notinheap
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 //span里面的管理的页数
manualFreeList gclinkptr // list of free objects in mSpanManual spans
begin
freeindex uintptr
nelems uintptr
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.
}
//TODO 每个mspan的页数如何确定的,
span class:指span的级别,但span class的大小与span的大小并没有正比关系。span class主要用来和size class做对应,1个size class对应2个span class,2个span class的span大小相同,只是功能不同,1个用来存放包含指针的对象,一个用来存放不包含指针的对象,不包含指针对象的Span就无需GC扫描了。
Go内存分配由内存分配器完成。分配器由3种组件构成:mcachemcentralmheap
mcache
mcache:每个工作线程都会绑定一个mcache,每个P(MPG的P)都有一个mcache,本地缓存可用的mspan资源,这样就可以直接给Goroutine分配,因为不存在多个Goroutine竞争的情况,所以不会消耗锁资源
mcache的结构体定义
//path: /usr/local/go/src/runtime/mcache.go
type mcache struct {
alloc [numSpanClasses]*mspan
}
numSpanClasses = _NumSizeClasses << 1
mcacheSpan Classes作为索引管理多个用于分配的mspan,它包含所有规格的mspan。它是_NumSizeClasses的2倍,也就是67*2=134,为什么有一个两倍的关系?
为了加速之后内存回收的速度,数组里一半的mspan中分配的对象不包含指针,另一半则包含指针。
对于无指针对象的mspan在进行垃圾回收的时候无需进一步扫描它是否引用了其他活跃的对象
mcache在初始化的时候是没有任何mspan资源的,在使用过程中会动态地从mcentral申请,之后会缓存下来。当对象小于等于32KB大小时,使用mcache的相应规格的mspan进行分配
mcentral
mcentral:为所有mcache提供切分好的mspan资源。每个mcentral保存一种特定大小的全局mspan列表,包括已分配出去的和未分配出去的。 每个mcentral对应一种mspan,而mspan的种类导致它分割的object大小不同。当工作线程的mcache中没有合适(也就是特定大小的)的mspan时就会从mcentral获取
mcentral被所有的工作线程共同享有,存在多个Goroutine竞争的情况,因此会消耗锁资源。结构体定义
type mcentral struct {
// 互斥锁
lock mutex
// 规格
sizeclass int32
// 尚有空闲object的mspan链表
nonempty mSpanList
// 没有空闲object的mspan链表,或者是已被mcache取走的msapn链表
empty mSpanList
// 已累计分配的对象个数
nmalloc uint64
}
empty表示这条链表里的mspan都被分配了object,或者是已经被cache取走了的mspan,这个mspan就被那个工作线程独占了。而nonempty则表示有空闲对象的mspan列表。每个central结构体都在mheap中维护。
简单说下mcachemcentral获取和归还mspan的流程:
  • 获取 加锁;从nonempty链表找到一个可用的mspan;并将其从nonempty链表删除;将取出的mspan加入到empty链表;将mspan返回给工作线程;解锁。
  • 归还 加锁;将mspanempty链表删除;将mspan加入到nonempty链表;解锁
mheap
mheap:代表Go程序持有的所有堆空间,Go程序使用一个mheap的全局对象_mheap来管理堆内存。
mcentral没有空闲的mspan时,会向mheap申请。而mheap没有资源时,会向操作系统申请新内存。mheap主要用于大对象的内存分配,以及管理未切割的mspan,用于给mcentral切割成小对象。
同时我们也看到,mheap中含有所有规格的mcentral,所以,当一个mcachemcentral申请mspan时,只需要在独立的mcentral中使用锁,并不会影响申请其他规格的mspan
总结:32KB 的对象,直接从mheap上分配;
<=16B 的对象使用mcache的tiny分配器分配;
(16B,32KB] 的对象,首先计算对象的规格大小,然后使用mcache中相应规格大小的mspan分配;
如果mcache没有相应规格大小的mspan,则向mcentral申请
如果mcentral没有相应规格大小的mspan,则向mheap申请
如果mheap中也没有合适大小的mspan,则向操作系统申请
mcache, mcentral, mheap是Go内存管理的三大组件,层层递进。mcache管理线程在本地缓存的mspan;mcentral管理全局的mspan供所有线程使用;mheap管理Go的所有动态分配内存
参考链接:
https://juejin.im/post/6844903795739082760

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注