golang内存管理
前言
golang实现了自己的内存管理,在研究切片扩容的时候,有一步roundsize
调整内存大小方法,随后对golang中的内存管理深入学习了下,固记录。
概念
在程序启动的时候,golang会预先向处理器申请,如下虚拟地址空间(并没有真正的申请),主要将其规划给spans、bitmap、arena三部分
+-----------------------------------------------------------+
| spans | bitmap | arena |
+-----------------------------------------------------------+
arena:就是说的内存堆区,真正分配内存的地方,大小为512G,内部由多个page分割,page是内存存储的基本单元(8KB大小)
spans:存放多个*span,span是内存管理的基本单元,后文提及。需要确保每个span都能够指向不同的page位置,所以spans的大小为(512G/8KB)*每个指针大小8B=512M
bitmap:保存gc相关的位图信息
object:代码中等待分配的具体对象,在arena区域分配内存(一个object可能比page大也可能比page小)
策略
golang中的内存分配策略主要参考了C语言中的tcmalloc
,将存储对象(object)分为大小不同的多种,分别进行管理,减少内存碎片的问题,在golang中主要划分为67种类别class
// class bytes/obj bytes/span objects tail waste max waste
// 1 8 8192 1024 0 87.50% 7/8
// 2 16 8192 512 0 43.75% 7/16
// 3 32 8192 256 0 46.88% 15/32
// 4 48 8192 170 32 31.52% ....
// 5 64 8192 128 0 23.44%
// 6 80 8192 102 32 19.07%
// 7 96 8192 85 32 15.95%
// 8 112 8192 73 16 13.56%
// 9 128 8192 64 0 11.72%
// 10 144 8192 56 128 11.82%
// 11 160 8192 51 32 9.73%
// 12 176 8192 46 96 9.59%
// 13 192 8192 42 128 9.25%
// 14 208 8192 39 80 8.12%
// 15 224 8192 36 128 8.15%
// 16 240 8192 34 32 6.62%
// 17 256 8192 32 0 5.86%
// 18 288 8192 28 128 12.16%
// 19 320 8192 25 192 11.80%
// 20 352 8192 23 96 9.88%
// 21 384 8192 21 128 9.51%
// 22 416 8192 19 288 10.71%
// 23 448 8192 18 128 8.37%
// 24 480 8192 17 32 6.82%
// 25 512 8192 16 0 6.05%
// 26 576 8192 14 128 12.33%
// 27 640 8192 12 512 15.48%
// 28 704 8192 11 448 13.93%
// 29 768 8192 10 512 13.94%
// 30 896 8192 9 128 15.52%
// 31 1024 8192 8 0 12.40%
// 32 1152 8192 7 128 12.41%
// 33 1280 8192 6 512 15.55%
// 34 1408 16384 11 896 14.00%
// 35 1536 8192 5 512 14.00%
// 36 1792 16384 9 256 15.57%
// 37 2048 8192 4 0 12.45%
// 38 2304 16384 7 256 12.46%
// 39 2688 8192 3 128 15.59%
// 40 3072 24576 8 0 12.47%
// 41 3200 16384 5 384 6.22%
// 42 3456 24576 7 384 8.83%
// 43 4096 8192 2 0 15.60%
// 44 4864 24576 5 256 16.65%
// 45 5376 16384 3 256 10.92%
// 46 6144 24576 4 0 12.48%
// 47 6528 32768 5 128 6.23%
// 48 6784 40960 6 256 4.36%
// 49 6912 49152 7 768 3.37%
// 50 8192 8192 1 0 15.61%
// 51 9472 57344 6 512 14.28%
// 52 9728 49152 5 512 3.64%
// 53 10240 40960 4 0 4.99%
// 54 10880 32768 3 128 6.24%
// 55 12288 24576 2 0 11.45%
// 56 13568 40960 3 256 9.99%
// 57 14336 57344 4 0 5.35%
// 58 16384 16384 1 0 12.49%
// 59 18432 73728 4 0 11.11%
// 60 19072 57344 3 128 3.57%
// 61 20480 40960 2 0 6.87%
// 62 21760 65536 3 256 6.25%
// 63 24576 24576 1 0 11.45%
// 64 27264 81920 3 128 10.00%
// 65 28672 57344 2 0 4.91%
// 66 32768 32768 1 0 12.50%
class:类别
bytes/obj:该class类别每个对象的大小
bytes/span:该class类别span的总大小
objects:可以存放的对象数 (bytes/span)/(bytes/obj)
max waste:最大浪费空间,example:大小为1B的对象为其分配8B的内存空间,浪费7/8=87.5%
bytes/obj满足8*2n的关系,至于为什么是这个关系?设计考虑到整体max waste需小于某个值,在golang中这个值为多少没有具体了解
span
span是内存管理的基本单元,由1个或者多个page组成,page个数在go中也是写死的
// runtime/sizeclasses.go
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}
var class_to_allocnpages = [_NumSizeClasses]uint8{0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 3, 2, 3, 1, 3, 2, 3, 4, 5, 6, 1, 7, 6, 5, 4, 3, 5, 7, 2, 9, 7, 5, 8, 3, 10, 7, 4}
// runtime/mheap.go
type mspan struct {
next *mspan
prev *mspan
list *mSpanList
startAddr uintptr // mspan在areana中的开始地址
npages uintptr // span的page数
freeindex uintptr // 下一个可使用object的索引
nelems uintptr // span中object的个数
spanclass spanClass // class
elemsize uintptr // class表中的对象大小,也即块大小
allocBits uintptr //分配位图,每一位代表一个块是否已分配
allocCount uint16 // 已分配块的个数
}
span在源码中的数据结构对应mspan,只写了一部分比较重要的字段,后文源码展示同理。
可以看出mspan是一个双向的链表,保存其在arena中开始位置startAddr
、分配页数npages
、span中存放的object信息等…
spans中*span指针就指向该结构体
基本单元
内存管理主要通过mcache
、mcentral
、mheap
进行配合
mcache
// runtime/mcache.go
type mcache struct {
// 每种类别是一个链表
// class0 scan class0 noscan
// | |
// | |
// class0 scan class0 noscan
// 每一个mcache有134种mspan(67个指针供指针)
// 每一个mspan可以分配多个object
alloc [67*2]*mspan
}
mcache
就是管理mspan
的基本单元,alloc
字段为长度67*2的的*mspan数组,大小为classsize的两倍,原因是go在处理内存时将指针对象和非指针对象区分处理了,对于包含指针的对象需要进行扫描操作scan,不包含指针的对象不需要进行多余的扫描操作noscan,也是一种性能优化方案。每个class对应一个span链表,需要注意的是该链表对应的都是该class的span,如注释中那样。
在程序运行时每个线程P会与一个mcache进行绑定(GMP调度模型),所以mcache
不存在锁问题,在P运行初始是不存在任何span的
mcentral
mcentral
是内存分配的中转站(central),每个class对应一个mcentral。当某个mcache
无内存可用时到相应class的mcentral
中获取,所以mcentral
是被多个线程共享的,需要锁
// runtime/mcentral.go
type mcentral struct {
// 多个goroutine共享的拿mspan所以需要锁
lock sync.Mutex
spanclass spanClass
// 维护两个双向链表
// 当有mcache申请一个mspan时
// 遍历链表查找可利用的mspan
// 当没有mspan可用时,mcentral向mheap申请并返回给mcache
nonempty mSpanList // 还有空闲对象或者已经被mcache缓存的mspan
empty mSpanList // 没有空闲对象的mspan联表
}
mcentral
维护两个双向链表nonempty
还有空闲对象的span链表和empty
没有空闲对象的span链表。
mheap
mheap.central
字段保存67*2个mcentral
,程序运行开始预先分配的bitmap
、spans
、arena
就是通过mheap
进行管理,mheap
作为上层结果管理所有的内存空间
// runtime/mheap.go
type mheap struct {
// mheap对所有线程共享,固需锁
lock sync.Mutex
allspans []*mspan // 所有mspan
bitmap uintptr //指向bitmap首地址,bitmap是从高地址向低地址增长的
arena_start uintptr //指示arena区首地址
arena_used uintptr //指示arena区已使用地址位置
// heap由134个mcentral组成,67个无指针mcentral
// 67个有指针mcentral
// 每个class 一个mcentral
central [134]struct {
mcentral mcentral
}
}
每个class类别对应一个mcentral
,一共有67*2(scan和noscan两种)种class也就是134种mcentral
mheap
对所有线程共享固需加锁
内存分配流程
前文提及的对象大小划分图种只列出了66种,go种将对象大小划分为67种,最后一种为class为0的类别,bytes/obj>32KB的对象,此种对象称为大对象,直接由mheap
进行分配、另外就是小于16B的对象称为tiny对象,go会使用tinyalloc
分配器进行分配,本文着重讲[16B,32KB]的对象的内存分配管理
当线程需要获取大小为N的内存时:
- 判断N大小属于两个class类别区间,向上取整获取需为其分配的内存空间大小,example需要为bool类型的1B大小分配内存空间向上取整为8B(所以会有一部分内存的浪费)
- 获取当前线程P绑定的
mcache
- 通过对象大小获取class ID,在
mcache.alloc[class]
中获取相应class的span
链表,判断是否还有可用的object空间 - 如果没有可用的span空间则向相应class的
mcentral
申请span
线程从mcentral
获取span:
mcentral
加锁mcentral
从nonempty中取出可用span并删除,empty中添加相应span,如果没有可利用的span则向mheap
获取mcentral
返回span
mcache
拿到可利用span则获取空闲object区域,返回地址
线程将span返还给mcentral
过程:
mcentral
加锁- 从empty中删除相应span,nonempty中添加相应大小span
mcentral
解锁
总结
- go程序启动时预先申请一片虚拟地址空间自行管理
- 将地址空间分为
bitmap
、spans
、arena
三个区域,三片区域都通过mheap
数据结构进行统一管理 spans
保存指向mspan
的指针,mspan
内存管理的基本单位,主要以链表方式展现。bitmap
保存gc相关的位图信息,比如哪些被gc扫描过,哪些没有。arena
是就是保存对象的地址堆区span
由一个或多个页page
组成,在go中已经被定死了,真正存储的对象为object
,被分为大小不同的67种mcache
与线程绑定,线程需要获取内存时首先从mcache
获取,不存在锁mcentral
供多个线程共享span,当mcache
没有可用span时从mcentral
获取
参考资料
[图解go内存分配]:https://juejin.cn/post/6844903795739082760
[Go’s Memory Allocator - Overview]:https://andrestc.com/post/go-memory-allocation-pt1/