文章目录
虚拟内存空间:堆区和栈区
栈区:函数调用的参数、返回值、局部变量,这部分内存由编译器管理
堆区:堆中的对象由内存分配器分配,垃圾收集器回收
内存管理的三个组件:用户程序、分配器、收集器
用户程序通过分配器在堆上初始化内存空间
一、分配方法
线性分配器、空闲链表分配器
1、线性分配:只需维护指针
缺点:无法在对象释放后重用该空间
2、空闲链表分配
可重用被释放的内存,内部维护空闲链表(数据结构),用户程序申请内存时会遍历链表找到足够大的内存块
类似策略:隔离适应,将内存划分为多个内存块大小不同的链表,单个链表内内存块大小相同,优点是减少了遍历内存块数量,提高效率
3、分级分配:Golang
Golang内存分配器根据申请分配内存的大小选择不同的处理逻辑,将对象分为三类
类别 | 大小 |
---|---|
微对象 | (0,16B) |
小对象 | [16B,32KB] |
大对象 | (32KB,+∞) |
多级缓存
内存分配器不仅区别对待大小不同的对象,还会将内存分为不同的级别管理:线程缓存(Thread Cache)
、中心缓存(Central Cache)
和页堆(Page Heap)
三级
线程缓存属于单个线程,不涉及多线程,因此不用加锁,如果线程缓存不满足分配,小对象由中心缓存分配,大对象直接放入页堆,与操作系统的多级缓存类似
二、虚拟内存布局
Go在1.10以前的版本堆区的内存空间是连续的,但在1.11版本,使用稀疏的堆内存空间取代了连续内存
线性内存
1.10版本之前,程序启动前会初始化虚拟内存空间,spans、bitmap、arena
spans:存储了指向内存管理单元runtime.mspan的指针,每个内存单元由n*8KB的页组成
bitmap:用于标识arena的那些地址保存了对象,位图中的每个byte都会表示堆区中的32byte是否空闲
arena:真正的堆区,运行时8K为一页
任何一个地址,可以通过arena的基地址计算页数,并通过spans数组找到对应的runtime.mspan,spans数组中多个连续的位置可能对应同一个mspan。Go的垃圾回收会通过指针的地址找到管理该对象的runtime.mspan。
问题: C和Go混合使用会导致程序崩溃
稀疏内存
移除堆内存512G的上限,解决了C和Go混合使用的地址冲突问题
二维稀疏内存,使用二维的runtime.heapArena数组管理内存,每个单元管理64MB的内存空间:
type heapArena struct {
//是否空闲
bitmap [heapArenaBitmapBytes]byte
//内存单元
spans [pagesPerArena]*mspan
pageInUse [pagesPerArena / 8]uint8
pageMarks [pagesPerArena / 8]uint8
pageSpecials [pagesPerArena / 8]uint8
checkmarks *checkmarksMap
//该结构体的基地址
zeroedBase uintptr
}
三、Go内存管理组件
对应的数据结构
内存管理单元 – runtime.mspan
线程缓存 – runtime.mcache
中心缓存 – runtime.mcentral
页堆 – runtime.mheap
内存管理单元-mspan
//go:notinheap
type mspan struct {
next *mspan //链表下一个span地址
prev *mspan // 链表前一个span地址
list *mSpanList // 链表地址
startAddr uintptr // 该span在arena区域的起始地址
npages uintptr // 该span占用arena区域page的数量
manualFreeList gclinkptr // 空闲对象列表
freeindex uintptr//freeindex是0到nelems之间的位置索引,标记下一个空对象索引
nelems uintptr // 管理的对象数
allocCache uint64 //从freeindex开始的位标记
allocBits *gcBits //该mspan中对象的位图
gcmarkBits *gcBits //该mspan中标记的位图,用于垃圾回收
sweepgen uint32 //扫描计数值,用户与mheap的sweepgen比较,根据差值确定该span的扫描状态
allocCount uint16 // 已分配的对象的个数
spanclass spanClass // span分类
state mSpanState // mspaninuse etc
needzero uint8 // 分配之前需要置零
scavenged bool // 标记是否内存已经被系统回收,大对象会用到
elemsize uintptr // 对象的大小
unusedsince int64 // 空闲状态开始的纳秒值时间戳,用于系统内存释放
limit uintptr // 申请大对象内存块会用到,mspan的数据截止位置
Go语言内存管理的基本单元,包含前后指针,运行时用mSpanList串联
mspan会根据spanclass把空间划分为一个个对象,分配空间就是把这一个个小对象分配出去,其中freeindex记录了第一个空对象。
spanclass一共有67种
线程缓存-mcache
type p struct {
···
mcache *mcache
···
}
//go:notinheap
type mcache struct {
tiny uintptr //<16byte 申请小对象的起始地址
tinyoffset uintptr //从起始地址tiny开始的偏移量
tinyAllocs uintptr //tiny对象分配的数量
alloc [numSpanClasses]*mspan // 分配的mspan list,其中numSpanClasses=134(实际上是68个,),索引是splanclassId
stackcache [_NumStackOrders]stackfreelist //栈缓存
flushGen uint32 //扫描计数,gc要用
}
- mcache会和处理器(P)绑定,用来给协程分配且只分配
小对象
,其中小对象还会细分为上述的size-class. - mcache是不会被GC的区域,所以GC时需要将mcache放到central cache中来回收。
- mcache只会给一个P使用,且一个P只会和一个M绑定,因此没有竞争,不需要加锁。
- mcache在初始化时不会分配空间,会在运行期间动态分配,如下图,小对象(16B~32KB)会在mcache中分配,分配的时候还会细分68个span-class(就是上面的
alloc
属性,当然其中有的不是小对象,忽略),不够的话从对应span-class的mcentral
申请;微对象会在heap中分配,mcache中有指向这块空间的指针,在mark termination阶段回收;大对象(>32KB)会直接在mheap中分配。
中心缓存-mcentral
上面说mcache会动态申请空间,就是在mcentral中申请,每个mcentral都有对应的span-class,mheap中有68*2个mcentral
//go:notinheap
type mcentral struct {
spanclass spanClass
// 这个原注解“a free object”就很魔性,按照我对google内存分配tcmalloc算法的理解,不论是thread cache还是central cache,如果空间不够就会从上层申请,但如果空间太多又会返回给上层,所以应该是每个spanclass都会保持一个空的object,这样就避免浪费空间,也能满足分配
partial [2]spanSet // list of spans with a free object
full [2]spanSet // list of spans with no free objects
}
// A spanSet is a set of *mspans.
//
// spanSet is safe for concurrent push and pop operations.
type spanSet struct {
spineLock mutex
spine unsafe.Pointer
spineLen uintptr
spineCap uintptr
index headTailIndex
}
type mheap struct {
···
central [numSpanClasses]struct {
mcentral mcentral
pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
}
···
- mcentral中有两个列表,分别是空闲列表和已经分配的列表;
- 当mcentral中也没有空闲区域时,就会从mheap中申请span,然后将span切分,增加到mcentral的空闲列表中;
堆-mheap
堆区的属性我还没完全理清,就不贴代码了
- 按上面所说,mcentral中空间不够后,会从mheap中取span,然后切分;
- mheap空间也不够的话,就会从操作系统申请;
- 上面所说的稀疏矩阵也有用到,分成一个个
heapArena
四、内存分配总结
所有对象均由runtime.newObject
函数分配,该函数调用runtime.mallocgc
分配指定大小的空间,按上述分级分配,分成三种对象:微对象、小对象、大对象。
微对象:在heap分配,有一个指针指向它,在mark termination阶段回收。
小对象:mcache中分配,细分为68中spanclass,不够从central cache申请
大对象:直接在mheap分配,不够从操作系统申请