操作系统的存储管理
虚拟内存管理
虚拟内存是一种内存管理技术,它允许操作系统为每个进程提供一个比实际物理内存更大的地址空间。这个地址空间被称为虚拟地址空间,而实际的物理内存则被称为物理地址空间。使用虚拟内存有以下几点好处:
内存抽象:虚拟内存为每个进程提供了一个连续的地址空间,使得每个程序都认为自己独占了整个内存,这样可以简化程序的编写和调试,开发人员不需要关心物理内存的具体布局。
内存保护:通过虚拟内存,每个进程都有自己的地址空间,互不干扰。这样可以防止一个程序错误地访问或修改另一个程序的内存,提高了系统的稳定性和安全性。
内存共享:虚拟内存允许多个进程共享相同的物理内存页,例如,多个进程可以共享相同的库或程序代码,这样可以节省内存空间。
支持更多的应用程序:虚拟内存通过使用硬盘空间作为额外的内存,在物理内存不足时,可以临时将不常用的内存页移出到硬盘上,从而释放内存空间。这样,系统就可以运行更多的应用程序,即使物理内存有限。
更有效的内存使用 :操作系统可以通过虚拟内存管理机制,如页面置换算法,来优化内存的使用效率,确保最常用的数据和程序保留在物理内存中,而较少使用的部分则可以暂时存储在硬盘上。
进程的虚拟内存管理
每个进程的虚拟内存空间都会映射到物理内存,如何映射对于程序员来说是透明的不需要关心,虚拟内存到物理内存的映射由操作系统实现。内核态虚拟内存空间是所有进程共享的,不同进程进入内核态之后看到的虚拟内存空间全部是一样的。比如下图中进程 1、进程 2 在内核态都去访问虚拟地址,由于内核态这部分地址空间映射到的都是同一份物理内存,进程 1、进程 2 看到的内容也是一样的。
进程的用户空间被分为多个段:
-
代码段:在进程运行之前,存放在二进制文件中的机器码需要被加载进内存中,用于存放这些机器码的虚拟内存空间叫做代码段。
-
数据段:指定初始值的全局变量和静态变量,在程序运行之前这些全局变量和静态变量会被加载进内存中供程序访问,这段区域叫做数据段。
-
BBS段:没有指定初始值的全局变量和静态变量在虚拟内存空间中的存储区域叫做 BSS 段,这些未初始化的全局变量被加载进内存之后会被初始化为 0 值。BSS 段与数据段的区别是不会一开始就加入到内存中而是需要的时候从硬盘上加载。
-
堆:程序在运行期间往往需要动态的申请内存,所以在虚拟内存空间中也需要一块区域来存放这些动态申请的内存,这块区域就叫做堆。
-
栈:在程序运行的时候会调用函数,调用函数过程中使用到的局部变量和函数参数也需要一块内存区域来保存。这一块区域在虚拟内存空间中叫做栈。
-
文件映射与匿名映射区:每当我们调用一次 mmap 进行内存映射的时候,内核都会在文件映射与匿名映射区中划分出一段虚拟映射区出来,这段虚拟映射区就是我们申请到的虚拟内存。
分页管理
操作系统中通常会将虚拟内存和物理内存切割成固定的尺寸,于虚拟内存而言叫作"页",于物理内存而言叫作"帧",原因及要点如下:
- 提高内存空间利用(以页为粒度后,消灭了不稳定的外部碎片,取而代之的是相对可控的内部碎片)
- 提高内外存交换效率(更细的粒度带来了更高的灵活度)
- 与虚拟内存机制呼应,便于建立虚拟地址->物理地址的映射关系(聚合映射关系的数据结构,称为页表)
- linux 页/帧的大小固定,为 4KB(这实际是由实践推动的经验值,太粗会增加碎片率,太细会增加分配频率影响效率)
golang的内存管理
基础概念
为了方便自主管理内存, 做法便是先向系统申请一块内存, 然后将内存切割成小块, 通过一定的内存分配算法管理内存。 以64位系统为例,Golang程序启动时会向系统申请的内存如下图所示:
预申请的内存划分为spans、 bitmap、 arena三部分。 其中arena即为所谓的堆区, 应用中需要的内存从这里分配。其中spans和bitmap是为了管理arena区而存在的。
arena的大小为512G, 为了方便管理把arena区域划分成一个个的page, 每个page为8KB,一共有512GB/8KB个页;
spans区域存放span的指针, 每个指针对应一个page, 所以span区域的大小为(512GB/8KB)*指针大小8byte =512M
bitmap区域大小也是通过arena计算出来, 不过主要用于GC。
内存池概念
在程序中变量的存储位置通常分布在全局数据区、栈区和堆区这三个内存区域:
- 堆区: 主要承担运行时的动态内存分配任务,它为程序提供灵活的内存使用空间。
- 全局数据区: 专门用于存储全局变量,这些变量在程序的整个生命周期内都存在。
- 栈区: 以栈帧为基本单位进行分配,它负责存储函数的参数、返回值以及局部变量。
golang的内存架构
golang的内存分配主要有三个组件
mcache :Per-P(Processer,具体参见go中G,M,P的概念)私有cache,用于实现无锁的object分配。
mcentral :全局内存,为各个cache提供按大小划分好的span,每种对象大小规格(全局共划分为 68 种)对应的缓存,锁的粒度也仅限于同一种规格以内
mheap:全局内存,page管理,内存不足时向系统申请,访问要加全局锁
page和mspan的概念
在此梳理一下span和mspan的概念:
-
page:最小的存储单元,Golang 借鉴操作系统分页管理的思想,每个最小的存储单元也称之为页 page,但大小为 8 KB。
-
mspan: 大小为 page 的整数倍,且从 8B 到 80 KB 被划分为 67 种不同的规格,分配对象时,会根据大小映射到不同规格的 mspan,从中获取空间。
mspan 的数据结构
span是内存管理的基本单位,每个span用于管理特定的class对象, 跟据对象大小, span将一个或多个页拆分成多个块进行管理。源码位于 runtime/mheap.go 文件中:
go
type mspan struct {
// 标识前后节点的指针
next *mspan
prev *mspan
// ...
// 起始地址
startAddr uintptr
// 包含几页,页是连续的
npages uintptr
// 标识此前的位置都已被占用
freeindex uintptr
// 最多可以存放多少个 object
nelems uintptr // number of object in the span.
allocBits *gcBits //分配位图, 每一位代表一个块是否已分配
allocCount uint16 // 已分配块的个数
// bitmap 每个 bit 对应一个 object 块,标识该块是否已被占用
allocCache uint64
// ...
// 标识 mspan 等级,包含 class 和 noscan 两部分信息
spanclass spanClass
elemsize uintptr // class表中的对象大小, 也即块大小
// ...
}
以class 10为例, span和管理的内存如下图所示:
spanclass为10, 参照class表(下文有)可得出npages=1,nelems=56,elemsize为144。 其中startAddr是在span初始化时就指定了某个页的地址。 allocBits指向一个位图, 每位代表一个块是否被分配, 本例中有两个块已经被分配,其allocCount也为2。next和prev用于将多个span链接起来, 这有利于管理多个span。
mspan 根据空间大小和面向分配对象的大小,被划分为 67 种等级(1-67,实际上还有一种隐藏的 0 级,用于处理更大的对象,上不封顶
bash
// sizeclass
// class bytes/obj bytes/span objects waste bytes
// 1 8 8192 1024 0
// 2 16 8192 512 0
// 3 32 8192 256 0
// 4 48 8192 170 32
// 5 64 8192 128 0
// 6 80 8192 102 32
// 7 96 8192 85 32
// 8 112 8192 73 16
// 9 128 8192 64 0
// 10 144 8192 56 128
// 11 160 8192 51 32
// 12 176 8192 46 96
// 13 192 8192 42 128
// 14 208 8192 39 80
// 15 224 8192 36 128
// 16 240 8192 34 32
// 17 256 8192 32 0
// 18 288 8192 28 128
// 19 320 8192 25 192
// 20 352 8192 23 96
// 21 384 8192 21 128
// 22 416 8192 19 288
// 23 448 8192 18 128
// 24 480 8192 17 32
// 25 512 8192 16 0
// 26 576 8192 14 128
// 27 640 8192 12 512
// 28 704 8192 11 448
// 29 768 8192 10 512
// 30 896 8192 9 128
// 31 1024 8192 8 0
// 32 1152 8192 7 128
// 33 1280 8192 6 512
// 34 1408 16384 11 896
// 35 1536 8192 5 512
// 36 1792 16384 9 256
// 37 2048 8192 4 0
// 38 2304 16384 7 256
// 39 2688 8192 3 128
// 40 3072 24576 8 0
// 41 3200 16384 5 384
// 42 3456 24576 7 384
// 43 4096 8192 2 0
// 44 4864 24576 5 256
// 45 5376 16384 3 256
// 46 6144 24576 4 0
// 47 6528 32768 5 128
// 48 6784 40960 6 256
// 49 6912 49152 7 768
// 50 8192 8192 1 0
// 51 9472 57344 6 512
// 52 9728 49152 5 512
// 53 10240 40960 4 0
// 54 10880 32768 3 128
// 55 12288 24576 2 0
// 56 13568 40960 3 256
// 57 14336 57344 4 0
// 58 16384 16384 1 0
// 59 18432 73728 4 0
// 60 19072 57344 3 128
// 61 20480 40960 2 0
// 62 21760 65536 3 256
// 63 24576 24576 1 0
// 64 27264 81920 3 128
// 65 28672 57344 2 0
// 66 32768 32768 1 0
在 Golang 中,会将 spanClass + nocan 两部分信息组装成一个 uint8,形成完整的 spanClass 标识. 8 个 bit 中,高 7 位表示了上表的 span 等级(总共 67 + 1 个等级,8 个 bit 足够用了),最低位表示 nocan 信息。代码位于 runtime/mheap.go
go
type spanClass uint8
const (
numSpanClasses = _NumSizeClasses << 1
tinySpanClass = spanClass(tinySizeClass<<1 | 1)
)
// uint8 左 7 位为 mspan 等级,最右一位标识是否为 noscan
func 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
管理内存的基本单位span, 还要有个数据结构来管理span, 这个数据结构叫mcentral, 各线程需要内存时从mcentral管理的span中申请内存, 为了避免多线程申请内存时不断的加锁, Golang为每个线程分配了span的缓存, 这个缓存即是mcache,代码位于 runtime/mcache.go:
go
const numSpanClasses = 136
type mcache struct {
// 微对象分配器相关
tiny uintptr
tinyoffset uintptr
tinyAllocs uintptr
// mcache 中缓存的 mspan,每种 spanClass 各一个
alloc [numSpanClasses]*mspan
// ...
}
(1)mcache 是每个 P 独有的缓存,因此交互无锁
(2)mcache 将每种 spanClass 等级的 mspan 各缓存了一个,总数为 2(nocan 维度) * 68(大小维度)= 136
(3)mcache 中还有一个为对象分配器 tiny allocator,用于处理小于 16B 对象的内存分配。
中心缓存 mcentral
mcache作为线程的私有资源为单个线程服务, 而central则是全局资源, 为多个线程服务, 当某个线程内存不足时会向mcentral申请, 当某个线程释放内存时又会回收进mcentral。代码位于 runtime/mcentral.go
go
type mcentral struct {
// 对应的 spanClass
spanclass spanClass
// 有空位的 mspan 集合,数组长度为 2 是用于抗一轮 GC
partial [2]spanSet
// 无空位的 mspan 集合
full [2]spanSet
}
(1)每个 mcentral 对应一种 spanClass
(2)每个 mcentral 下聚合了该 spanClass 下的 mspan
(3)mcentral 下的 mspan 分为两个链表,分别为有空间 mspan 链表 partial 和满空间 mspan 链表 full
(4)每个 mcentral 一把锁
全局堆缓存 mheap
从mcentral数据结构可见, 每个mcentral对象只管理特定的class规格的span。 事实上每种class都会对应一个mcentral,这个mcentral的集合存放于mheap数据结构中。代码位于 runtime/mheap.go
go
type mheap struct {
// 堆的全局锁
lock mutex
// 空闲页分配器,底层是多棵基数树组成的索引,每棵树对应 16 GB 内存空间
pages pageAlloc
// 记录了所有的 mspan. 需要知道,所有 mspan 都是经由 mheap,使用连续空闲页组装生成的
allspans []*mspan
// heapAreana 数组,64 位系统下,二维数组容量为 [1][2^22]
// 每个 heapArena 大小 64M,因此理论上,Golang 堆上限为 2^22*64M = 256T
arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
// ...
// 多个 mcentral,总个数为 spanClass 的个数
central [numSpanClasses]struct {
mcentral mcentral
// 用于内存地址对齐
pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
}
// ...
}
- 对于 Golang 上层应用而言,堆是操作系统虚拟内存的抽象
- 以页(8KB)为单位,作为最小内存存储单元
- 负责将连续页组装成 mspan
- 全局内存基于 bitMap 标识其使用情况,每个 bit 对应一页,为 0 则自由,为 1 则已被 mspan 组装
- 通过 heapArena 聚合页,记录了页到 mspan 的映射信息(2.7小节展开)
- 建立空闲页基数树索引 radix tree index,辅助快速寻找空闲页(2.6小节展开)
- 是 mcentral 的持有者,持有所有 spanClass 下的 mcentral,作为自身的缓存
- 内存不够时,向操作系统申请,申请单位为 heapArena(64M)
内存分配流程
tiny对象申请
对于tiny对象的申请,mcache中有专门的内存区域"tiny"来进行特殊处理。"tiny"将对象按大小与tinyoffset("tiny"当前分配地址)对齐,然后分配,并记录下新的tinyoffset,用于下次分配。如果空间不足,则另外申请16 byte的内存块。
go
noscan := typ == nil || typ.ptrdata == 0
// ...
if noscan && size < maxTinySize {
// tiny 内存块中,从 offset 往后有空闲位置
off := c.tinyoffset
// ...
// 如果当前 tiny 内存块空间还够用,则直接分配并返回
if off+size <= maxTinySize && c.tiny != 0 {
// 分配空间
x = unsafe.Pointer(c.tiny + off)
c.tinyoffset = off + size
c.tinyAllocs++
mp.mallocing = 0
releasem(mp)
return x
}
// ...
}
mcache 分配
go
// 根据对象大小,映射到其所属的 span 的等级(0~66)
var sizeclass uint8
// get size class ....
// 对应 span 等级下,分配给每个对象的空间大小(0~32KB)
// get span class
spc := makeSpanClass(sizeclass, noscan)
// 获取 mcache 中的 span
span = c.alloc[spc]
// 从 mcache 的 span 中尝试获取空间
v := nextFreeFast(span)
if v == 0 {
// mcache 分配空间失败,则通过 mcentral、mheap 兜底
v, span, shouldhelpgc = c.nextFree(spc)
}
// 分配空间
x = unsafe.Pointer(v)
在 mspan 中,基于 Ctz64 算法,根据 mspan.allocCache 的 bitMap 信息快速检索到空闲的 object 块,进行返回.
代码位于 runtime/malloc.go 文件中:
go
func nextFreeFast(s *mspan) gclinkptr {
// 通过 ctz64 算法,在 bit map 上寻找到首个 object 空位
theBit := sys.Ctz64(s.allocCache)
if theBit < 64 {
result := s.freeindex + uintptr(theBit)
if result < s.nelems {
freeidx := result + 1
if freeidx%64 == 0 && freeidx != s.nelems {
return 0
}
s.allocCache >>= uint(theBit + 1)
// 偏移 freeindex
s.freeindex = freeidx
s.allocCount++
// 返回获取 object 空位的内存地址
return gclinkptr(result*s.elemsize + s.base())
}
}
return 0
}
mcentral 分配
当 mspan 无可用的 object 内存块时,会步入 mcache.nextFree 方法进行兜底.
代码位于 runtime/mcache.go 文件中:
go
func (c *mcache) nextFree(spc spanClass) (v gclinkptr, s *mspan, shouldhelpgc bool) {
s = c.alloc[spc]
// ...
// 从 mcache 的 span 中获取 object 空位的偏移量
freeIndex := s.nextFreeIndex()
if freeIndex == s.nelems {
// ...
// 倘若 mcache 中 span 已经没有空位,则调用 refill 方法从 mcentral 或者 mheap 中获取新的 span
c.refill(spc)
// ...
// 再次从替换后的 span 中获取 object 空位的偏移量
s = c.alloc[spc]
freeIndex = s.nextFreeIndex()
}
// ...
v = gclinkptr(freeIndex*s.elemsize + s.base())
s.allocCount++
// ...
return
}
倘若 mcache 中,对应的 mspan 空间不足,则会在 mcache.refill 方法中,向更上层的 mcentral 乃至 mheap 获取 mspan,填充到 mache 中:
代码位于 runtime/mcache.go 文件中:
go
func (c *mcache) refill(spc spanClass) {
s := c.alloc[spc]
// ...
// 从 mcentral 当中获取对应等级的 span
s = mheap_.central[spc].mcentral.cacheSpan()
// ...
// 将新的 span 添加到 mcahe 当中
c.alloc[spc] = s
}
mcentral.cacheSpan 方法中,会加锁(spanClass 级别的 sweepLocker),分别从 partial 和 full 中尝试获取有空间的 mspan:
代码位于 runtime/mcentral.go 文件中:
go
func (c *mcentral) cacheSpan() *mspan {
// ...
var sl sweepLocker
// ...
sl = sweep.active.begin()
if sl.valid {
for ; spanBudget >= 0; spanBudget-- {
s = c.partialUnswept(sg).pop()
// ...
if s, ok := sl.tryAcquire(s); ok {
// ...
sweep.active.end(sl)
goto havespan
}
// 通过 sweepLock,加锁尝试从 mcentral 的非空链表 full 中获取 mspan
for ; spanBudget >= 0; spanBudget-- {
s = c.fullUnswept(sg).pop()
// ...
if s, ok := sl.tryAcquire(s); ok {
// ...
sweep.active.end(sl)
goto havespan
}
// ...
}
}
// ...
}
// ...
// 执行到此处时,s 已经指向一个存在 object 空位的 mspan 了
havespan:
// ...
return
}
mheap 分配
在 mcentral.cacheSpan 方法中,倘若从 partial 和 full 中都找不到合适的 mspan 了,则会调用 mcentral 的 grow 方法,将事态继续升级:
go
func (c *mcentral) cacheSpan() *mspan {
// ...
// mcentral 中也没有可用的 mspan 了,则需要从 mheap 中获取,最终会调用 mheap_.alloc 方法
s = c.grow()
// ...
// 执行到此处时,s 已经指向一个存在 object 空位的 mspan 了
havespan:
// ...
return
}
经由 mcentral.grow 方法和 mheap.alloc 方法的周转,最终会步入 mheap.allocSpan 方法中:
go
func (c *mcentral) grow() *mspan {
npages := uintptr(class_to_allocnpages[c.spanclass.sizeclass()])
size := uintptr(class_to_size[c.spanclass.sizeclass()])
s := mheap_.alloc(npages, c.spanclass)
// ...
// ...
return s
}
代码位于 runtime/mheap.go
go
func (h *mheap) alloc(npages uintptr, spanclass spanClass) *mspan {
var s *mspan
systemstack(func() {
// ...
s = h.allocSpan(npages, spanAllocHeap, spanclass)
})
return s
}
代码位于 runtime/mheap.go
go
func (h *mheap) allocSpan(npages uintptr, typ spanAllocType, spanclass spanClass) (s *mspan) {
gp := getg()
base, scav := uintptr(0), uintptr(0)
// ...此处实际上还有一阶缓存,是从每个 P 的页缓存 pageCache 中获取空闲页组装 mspan,此处先略去了...
// 加上堆全局锁
lock(&h.lock)
if base == 0 {
// 通过基数树索引快速寻找满足条件的连续空闲页
base, scav = h.pages.alloc(npages)
// ...
}
// ...
unlock(&h.lock)
HaveSpan:
// 把空闲页组装成 mspan
s.init(base, npages)
// 将这批页添加到 heapArena 中,建立由页指向 mspan 的映射
h.setSpans(s.base(), npages, s)
// ...
return s
}
向操作系统申请
倘若 mheap 中没有足够多的空闲页了,会发起 mmap 系统调用,向操作系统申请额外的内存空间.
代码位于 runtime/mheap.go 文件的 mheap.grow 方法中:
go
func (h *mheap) grow(npage uintptr) (uintptr, bool) {
av, asize := h.sysAlloc(ask)
}
func (h *mheap) sysAlloc(n uintptr) (v unsafe.Pointer, size uintptr) {
v = sysReserve(unsafe.Pointer(p), n)
}
func sysReserve(v unsafe.Pointer, n uintptr) unsafe.Pointer {
return sysReserveOS(v, n)
}
func sysReserveOS(v unsafe.Pointer, n uintptr) unsafe.Pointer {
p, err := mmap(v, n, _PROT_NONE, _MAP_ANON|_MAP_PRIVATE, -1, 0)
if err != 0 {
return nil
}
return p
}
mallocgc源码解读
代码位于 runtime/malloc.go 文件中:
go
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
// ...
// 获取 m
mp := acquirem()
// 获取当前 p 对应的 mcache
c := getMCache(mp)
var span *mspan
var x unsafe.Pointer
// 根据当前对象是否包含指针,标识 gc 时是否需要展开扫描
noscan := typ == nil || typ.ptrdata == 0
// 是否是小于 32KB 的微、小对象
if size <= maxSmallSize {
// 小于 16 B 且无指针,则视为微对象
if noscan && size < maxTinySize {
// tiny 内存块中,从 offset 往后有空闲位置
off := c.tinyoffset
// 如果大小为 5 ~ 8 B,size 会被调整为 8 B,此时 8 & 7 == 0,会走进此分支
if size&7 == 0 {
// 将 offset 补齐到 8 B 倍数的位置
off = alignUp(off, 8)
// 如果大小为 3 ~ 4 B,size 会被调整为 4 B,此时 4 & 3 == 0,会走进此分支
} else if size&3 == 0 {
// 将 offset 补齐到 4 B 倍数的位置
off = alignUp(off, 4)
// 如果大小为 1 ~ 2 B,size 会被调整为 2 B,此时 2 & 1 == 0,会走进此分支
} else if size&1 == 0 {
// 将 offset 补齐到 2 B 倍数的位置
off = alignUp(off, 2)
}
// 如果当前 tiny 内存块空间还够用,则直接分配并返回
if off+size <= maxTinySize && c.tiny != 0 {
// 分配空间
x = unsafe.Pointer(c.tiny + off)
c.tinyoffset = off + size
c.tinyAllocs++
mp.mallocing = 0
releasem(mp)
return x
}
// 分配一个新的 tiny 内存块
span = c.alloc[tinySpanClass]
// 从 mCache 中获取
v := nextFreeFast(span)
if v == 0 {
// 从 mCache 中获取失败,则从 mCentral 或者 mHeap 中获取进行兜底
v, span, shouldhelpgc = c.nextFree(tinySpanClass)
}
// 分配空间
x = unsafe.Pointer(v)
(*[2]uint64)(x)[0] = 0
(*[2]uint64)(x)[1] = 0
size = maxTinySize
} else {
// 根据对象大小,映射到其所属的 span 的等级(0~66)
var sizeclass uint8
if size <= smallSizeMax-8 {
sizeclass = size_to_class8[divRoundUp(size, smallSizeDiv)]
} else {
sizeclass = size_to_class128[divRoundUp(size-smallSizeMax, largeSizeDiv)]
}
// 对应 span 等级下,分配给每个对象的空间大小(0~32KB)
size = uintptr(class_to_size[sizeclass])
// 创建 spanClass 标识,其中前 7 位对应为 span 的等级(0~66),最后标识表示了这个对象 gc 时是否需要扫描
spc := makeSpanClass(sizeclass, noscan)
// 获取 mcache 中的 span
span = c.alloc[spc]
// 从 mcache 的 span 中尝试获取空间
v := nextFreeFast(span)
if v == 0 {
// mcache 分配空间失败,则通过 mcentral、mheap 兜底
v, span, shouldhelpgc = c.nextFree(spc)
}
// 分配空间
x = unsafe.Pointer(v)
// ...
}
// 大于 32KB 的大对象
} else {
// 从 mheap 中获取 0 号 span
span = c.allocLarge(size, noscan)
span.freeindex = 1
span.allocCount = 1
size = span.elemsize
// 分配空间
x = unsafe.Pointer(span.base())
}
// ...
return x
}
内存分配过程总结
针对待分配对象的大小不同有不同的分配逻辑:
- (0, 16B) 且不包含指针的对象: Tiny分配
- (0, 16B) 包含指针的对象: 正常分配
- [16B, 32KB] : 正常分配
- (32KB, -) : 大对象分配其中Tiny分配和大对象分配都属于内存管理的优化范畴, 这里暂时仅关注一般的分配方法。
以申请size为n的内存为例, 分配步骤如下:
- 获取当前线程的私有缓存mcache
- 跟据size计算出适合的class的ID
- 从mcache的alloc[class]链表中查询可用的span
- 如果mcache没有可用的span则从mcentral申请一个新的span加入mcache中
- 如果mcentral中也没有可用的span则从mheap中申请一个新的span加入mcentral
- 从该span中获取到空闲对象地址并返回
编程小Tips
首先看下哪些对象会分配到堆上?
- 堆上的指针只能指向堆上的对象
- interface、chan、map所持有或转化为interface的对象都会在堆上
- 函数内返回指针类型的,指针指向的对象会逃逸到堆上。
- string类型一定在堆上
- 切片扩容和拷贝
原则上,减少GC就是减少在堆上分配对象。
-
切片和map知道大小的,预先分配足够的大小,避免扩容。
-
尽量少生成string,使用切片的方式复用string。
-
少用反射,被反射的对象都会分配到堆上。
-
使用sync.Pool复用对象,避免重复生产销毁对象。
-
尽量让chan传递较小的对象,所有chan传递的对象都会分配到堆上。
第二个方法:调节GC频率
通过debug.SetGCPercent或环境变量GOGC设置堆空间增长率p,只有当堆空间达到上次GC完成时堆空间大小*(1+p)才会触发GC。
通过设置debug.SetMemoryLimit或环境变量GOMEMLIMIT设置软限制内存限制,这个条件会使go的GC频率动态调节,用于达到GOMEMLIMIT的需要,可用于防止OOM。但不会强制程序的内存使用量。
参考文档
Golang 内存模型与分配机制 - 小徐先生的文章 - 知乎
go 内存管理和GC详解 - 卡萨布兰卡的文章 - 知乎
操作系统虚拟内存管理(二):虚拟内存管理 - Mulily的文章 - 知乎
Golang内存管理(一):堆内存管理 - Mulily的文章 - 知乎