内存分配

内存分配

栈空间

动态按需分配内存,编译器为每一个函数调用都插入检查分配的内存是否够用,从而节省了用来防止栈空间溢出的保护页表,如果实际运行中超过了运行时分配的内存空间,会尝试进行栈拷贝

逃逸分析

如果变量的作用域没有逃离当前的函数,那么变量就会被优先分配在栈空间上,否则会被分配在堆中

逃逸的条件:

  • 发送指针或带指针的值到 channel 中
  • 在一个切片上存储指针或带指针的值
  • slice 的内部数组被重新分配了,数据扩充会导致逃逸到堆上
  • 使用 interface 调用方法

内存对齐

填充结构体保证对齐CPU机器字的整倍数

type                               size in bytes
------                             ------
byte, uint8, int8                  1
uint16, int16                      2
uint32, int32, float32             4
uint64, int64, float64, complex64  8
complex128                         16
uint, int                          4 or 8, depend on architectures
uintptr                            large enough to store the uninterpreted
                                   bits of a pointer value

64位需要 8 字节对齐,32位需要 4 字节对齐

初始化顺序

外部 import pkg -> 外部 const -> var -> init()

内存分配细节

  • Go在程序启动时,会向操作系统申请一大块内存,之后自行管理。
  • Go内存管理的基本单元是mspan,它由若干个页组成,每种mspan可以分配特定大小的object。
  • mcache, mcentral, mheap是Go内存管理的三大组件,层层递进。mcache管理线程在本地缓存的mspan;mcentral管理全局的mspan供所有线程使用;mheap管理Go的所有动态分配内存。
  • 极小对象会分配在一个object中,以节省资源,使用tiny分配器分配内存;一般小对象通过mspan分配内存;大对象则直接由mheap分配内存。

TCMalloc

Go 使用这种算法来进行内存的分配和释放,用运行时来解决堆内存碎片的问题

核心思想:将内存分成多个级来减少锁的粒度

线程内存管理

每一个内存页面被切分成多个不同大小的空闲链表,每条链表上节点持有的空间大小相同且固定,这种方式可以减少碎片,因此每一个线程会在本地有一个无锁的内存池用来分配小对象 (<= 32k)

堆的页面管理

TCMalloc 持有一个连续页面的集合,然后每个页面又可以用 span 来表示,大于 32 k的对象需要获得全局锁来从这里拿内存,如果不够,再向操作系统申请

Go 内存分配器

内存页面被分成了 67 种不同大小的内存块 Go的页面最小粒度为 8K,被称为一个 mspan

mcache

mspan的组织方式:双向链表,每个节点包含页的起始地址,大小和页的个数 每一个 P 在本地都持有每一种大小 (<=32K) 的 mspan 指针用来无锁分配内存 总共持有 134 种类型的 mspan 指针,用于含有指针的对象和不含指针的对象,其中一个好处就是垃圾回收的时候不用去遍历那些不含指针的对象

获取的时候优先本地 P -> 没有就找 mcentral

mcentral

存放了两个链表,一个 span 链表是在 Cache 中的 span 或没有空闲的 span,另一个 span 链表是有空闲的

mheap

每个 mcentral 又是包含在 mheap 中,每个 mcentral 通过 size class 来进行索引

分配策略

> 32k 的对象 — mheap < 16k 的对象 — mcache 的 tiny 分配器 16k ~ 32k — 优先根据 size class 在 mcache,没有再 mcentral 还是没有,通过最佳适配在 mheap 中找,再没有合适的才向 OS 申请几个新的页(至少1MB)

页面

Go 的每个页面是一个 arena,所有的堆内存来自于 arena ,arena 来自于操作系统分配的大小,调用 malloc 的时候操作系统会为主线程多分配一个 main arena(链表结构) 1.11.5 中初始的堆被映射在一个 arena 页中,大小为 64M(64位下)或 4M (32位) 每一个 arena 页由 8000 个 8KB 的mspan组成

内存泄露

十次泄露有九次都是 goroutine 的泄露 例如:不调用 resp.Body.Close()

timer 使用完之后不 stop