内存分配
栈空间
动态按需分配内存,编译器为每一个函数调用都插入检查分配的内存是否够用,从而节省了用来防止栈空间溢出的保护页表,如果实际运行中超过了运行时分配的内存空间,会尝试进行栈拷贝
逃逸分析
如果变量的作用域没有逃离当前的函数,那么变量就会被优先分配在栈空间上,否则会被分配在堆中
逃逸的条件:
- 发送指针或带指针的值到 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 value64位需要 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