Go内存分配
图解Go语言内存分配 - 知乎
go内置运行时,采用了自主管理,实现更好的内存使用模式,不需要每次内存分配都进行系统调用
采用TCMalloc
算法:把内存分为多级管理,从而降低锁的粒度
将可用的堆内存采用二级分配的方式进行管理:
每个线程都会自行维护一个独立的内存池,内存分配时优先从内存池中分配,内存池不足时向全局内存池申请,避免对全局内存池的频繁竞争
在程序启动时,向操作系统申请一块内存
arena:堆区,go动态分配内存,把内存分割为8kb大小的页,一些页组合称为mspan
bitmap:标识 arena 中哪些地址保存了对象,并用4bit标志位标识对象是否包含指针,GC标记信息
1个byte大小内存对应 arena区域中4个指针大小(指针大小8B)内存
bitmap的高地址指向arena的低地址,bitmap由高地址向低地址增长
span:存放mspan(arena 分割的页组合起来的内存管理基本单元)的指针,每个指针对应一页
创建mspan时,按页填充对应的spans区域
内存管理单元
mspan:内存管理的基本单元,由一片连续的8kb的页组成的大块内存,一般是操作系统页大小的几倍,包含起始地址、mspan
规格、页的数量等内容的双端链表
每个mspan按照自身的属性Size Class大小分割为若干个object,每个object存储一个对象
并且使用一个位图标记未使用的object
属性Size Class决定object大小,mspan只会分配给object尺寸大小接近的对象,对象大小小于object
每个Size Class两个mspan(span class)一个分配给含有指针的对象,一个分配个不含有指针对象
操作系统存储模型
多级模型 动态切换(因为数据的调用频率在实时发生变化)
虚拟内存与物理内存
在真实物理内存上的虚拟概念,用来代理,贴合用户使用视角
使用户(进程)觉得内存是连续的,而不是割裂的
实现内存“放大”的效果,虚拟内存可以由物理内存+磁盘补足,根据数据冷热进行动态置换
通过页表建立真实物理空间的映射关系
页表:聚合映射关系的数据结构
分页管理
虚拟内存中最小的操作单元:页
物理内存:帧
提高内存空间利用率(以页为粒度,外部碎片被替换为内部碎片,更相对可控)
提高内外交换效率
与虚拟内存机制呼应,建立虚拟地址到物理地址的映射关系
Golang内存模型
以空间换时间,一次缓存,多次复用
每次向操作系统多申请内存
堆mheap同样的思想:
对操作系统,用户进程中缓存的内存
对Go进程内部,堆是所有对象的内存起源
多级缓存,实现无/细锁化
mcentral 根据对象的大小从小到大排列等级(集合)
当分配内存给一个对象时,会根据其大小找到从属的等级,在对应的mcentral中尝试获取内存
mcache 每个处理器§单独的本地私有缓存,其中冗余每一种等级的内存空间
当尝试获取内存时,根据处理器,查看本地私有的mcache中是否有合适的空间使用,有则直接获取,无则访问mcentral,一级一级升级向上获取
多级规格,提高利用率
mcentral
page:go中有独立的内存存储单元,最小的存储单元 8KB
mspan:最小的管理单元,为page的整数倍,从8B到80KB分为67种不同的规格,分配对象时根据大小映射到不同规格的mspan,获取空间
隐藏的0级,处理更大的对象,上不封顶
mspan
属于mcentral的一个节点,根据span的不同等级有一个central实例,central存储的span个数是复数
stratAddr 起始地址 映射到Go语言中堆内存的一部分空间,也是虚拟内存中对应的地址
npages 页的页数,每个mspan中的页是连续的,可以根据起始地址+页数推导出,对应的内存区域
allocCache 基于bitmap辅助快速找到空闲内存块(块大小为对应等级下对象大小)
查找mspan中哪些页是空闲的,可以把对象分配到里面
type mspan struct {_ sys.NotInHeap// 标识前后节点的指针next *mspan prev *mspan // 起始地址startAddr uintptr// 包含页数,页是连续的npages uintptr // 标识此前的位置(bit位)都已被占用freeindex uint16// 最多可存放多少个对象(会出现一页存放多个对象的情况)nelems uint16// 每个bit对应一个对象块,标识该块是否被占用allocCache uint64// 标识mspan等级,包含class和noscan信息spanclass spanClass......
}
bytes/obj:该大小规格的对象会从当前等级的mspan中获取空间,创建对象过程中,大小会向上取整为8B的整数倍,可以直接实现对象到mspan等级映射
bytes/span:该等级的mspan的总空间大小
object:最多可以new多少个对象
max waste:
// nocan 标识对象是否包含指针,在gc时是否需要展开标记
// 存在指针需要展开扫描形成完整的有向图
// 会将两类对象,划分给不同的span(同一等级下的不同span)
// 将class+nocan组装为uint8,形成完整的spanClass 标识
// 高7位表示span等级,最低为表示nocan信息
type spanClass uint8func makeSpanClass(sizeclass uint8, noscan bool) spanClass {return spanClass(sizeclass<<1) | spanClass(bool2int(noscan))
}func (sc spanClass) sizeclass() int8 {return int8(sc >> 1)
}func (sc spanClass) noscan() bool {return sc&1 != 0
}
mcache
每个P独有的缓存,交互无锁
alloc 将每种spanClass等级的mspan各缓存一个,总数为2*68
对象分配器 tiny allocator 处理小于16B对象的内存分配
type mcache struct {_ sys.NotInHeap// 微对象分配器相关tiny uintptrtinyoffset uintptrtinyAllocs uintptr// mcache中缓存的mspan,每种各一个alloc [numSpanClasses]*mspan ......
}
mcentral中心缓存
每个central对应一个spanClass
会将mspan分为两个链表,有空间mspan链表partial和满空间mspan链表 full
type mcentral struct {_ sys.NotInHeap// 对应的spanClassspanclass spanClass// 有空位mspan集合// 数组长度为2,抗一轮GCpartial [2]spanSet // 无空位full [2]spanSet
}
mheap全局堆缓存
对于Golang上层应用,堆是操作系统虚拟内存的抽象
堆内存中最小内存存储单元:页(8KB)
负责将连续页组装为mspan
全局内存基于bitMap标识使用情况,一个bit对应一页,0自由,1已被组装为mspan
通过heapArena聚合页,记录页到mspan的映射信息,维护页所属的mspan关系
建立空闲页基数树索引 radix tree index 快速寻找空闲页,获取至少在虚拟视角下连续的空闲页
mcentral 的持有者,持有所有spanClass下mcentral,作为自身缓存,mcentral是mheap更细粒度的缓存(以空间换时间的操作)
内存不够,向操作系统申请,单位:heapArena(64M)
type mheap struct {_ sys.NotInHeap// 堆的全局锁(进程维度)lock mutex// 空闲页分配器,底层由多棵基数树组成的索引,每棵树对应16GB内存空间pages pageAlloc // 记录所有mspan// 所有mspan都是由mheap,使用连续空闲页组装allspans []*mspan // heapAreana 数组 请求内存和映射关系// 64位系统下,二维数组容量为[1][2^22]// 每个heapArena大小64M// 理论上堆的上限为 2^22*64M = 256Tarenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena// 多个mcentral,总数为spanClass 个数central [numSpanClasses]struct {mcentral mcentral// 用于内存地址对齐pad [(cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize) % cpu.CacheLinePadSize]byte}......
}