本文章仅供个人学习使用。
参考
- 小徐先生的编程世界:mp.weixin.qq.com/s/2TBwpQT5-...
- go语言设计与实现:draven.co/golang/docs...
笼统的概念
深入一件事情之前,先笼统了解其运用的思想。后续深入时,时刻想着这些思想,可以避免因为过于深入细节而忽略了整体导致的晕头转向。
空闲链表分配 + 按大小分类
空闲链表分配方式是区别于线性分配方式的。通过把空闲的内存空间串联成一个链表,实现内存分配。
这就涉及到一个问题:我要申请一块大小为x字节的内存,如何找到一块儿大于等于x的内存空间用来分配呢?
常见的算法有4种:
- 首次适应(First-Fit)--- 从链表头开始遍历,选择第一个大小大于申请内存的内存块;
- 循环首次适应(Next-Fit)--- 从上次遍历的结束位置开始遍历,选择第一个大小大于申请内存的内存块;
- 最优适应(Best-Fit)--- 从链表头遍历整个链表,选择最合适的内存块;
- 隔离适应(Segregated-Fit)--- 将内存分割成多个链表,每个链表中的内存块大小相同,申请内存时先找到满足条件的链表,再从链表中选择合适的内存块;
这四种,重点了解最后一种,因为go语言采用了与之类似的思想。

多级缓存
计算机世界很多地方都有多级缓存的体现:从 计算机硬件 到 大型web后端。Go的内存管理也用到了这种思想。
Go的内存管理,管理的是:虚拟内存。
Go的内存分配器,从用户侧到操作系统虚拟内存,依次是:
- 内存管理单元:mspan
- 线程缓存:mcache
- 中心缓存:mcentral
- 页堆:mheap

详细介绍内存管理组件
mspan
- mspan是内存管理的基本单元,管理的页大小为8KB。
- mspan管理的内存是连续的,所以知道了startAddress,又知道每个页大小固定=8KB,那再知道有多少页npages,就能确定mspan管理的内存范围了。
- Go划分了67种跨度,表示不同对象的大小,mspan的spanclass字段,标识了本span管理的是哪一个大小等级的对象。
- 管理相同大小等级的mspan串联在一起,组成双向链表。
- mspan中使用bitmap来标识哪些「对象」是空闲的。(注意不是页)
- 同等级的mspan从属于同一个mcentral,由同一把互斥锁管理。
管理相同大小等级的mspan串联在一起,组成双向链表:

mspan管理的内存是连续的,所以知道了startAddress,又知道每个页大小固定=8KB,那再知道有多少页npages,就能确定mspan管理的内存范围了:

mspan中使用bitmap来标识哪些「对象」是空闲的。(注意不是页)

最后我们来完整的看一眼go代码中的mspan:
            
            
              go
              
              
            
          
          type mspan struct {  
    // 标识前后节点的指针   
    next *mspan       
    prev *mspan      
    // 起始地址  
    startAddr uintptr   
    // 包含几页,页是连续的  
    npages    uintptr   
  
    // 标识此前的位置都已被占用   
    freeindex uintptr  
    
    // 最多可以存放多少个 object  
    nelems uintptr // number of object in the span.  
  
    // bitmap 每个 bit 对应一个 object 块,标识该块是否已被占用  
    allocCache uint64  
    
    // 标识 mspan 等级,包含 class 和 noscan 两部分信息  
    spanclass             spanClass      
    
    // ...  
}spanClass
跨度类。跨度类是一个8位无符号整数。高7位表示(67种跨度id),最低位表示noscan。
- noscan方法返回true --> 不扫描;否则扫描。
- 垃圾回收会对包含指针的 runtime.mspan结构体进行扫描。
- noscan位=1,方法返回true,所以- noscan= 1,表示对象不包含指针。
源码如下:
            
            
              go
              
              
            
          
          type spanClass uint8  
  
// 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  
}对象等级一览表:(其实一共68个等级,还有一种id=0表示超过32KB的对象)
- waste 挺好理解的:我想申请一个大小为9B的对象,class=1就不够,只能申请class=2的,那每申请一个对象就浪费16-9=7B。
| class | bytes/obj | bytes/span | objects | tail waste | max waste | 
|---|---|---|---|---|---|
| 1 | 8 | 8192 | 1024 | 0 | 87.50% | 
| 2 | 16 | 8192 | 512 | 0 | 43.75% | 
| 3 | 24 | 8192 | 341 | 0 | 29.24% | 
| 4 | 32 | 8192 | 256 | 0 | 46.88% | 
| 5 | 48 | 8192 | 170 | 32 | 31.52% | 
| 6 | 64 | 8192 | 128 | 0 | 23.44% | 
| 7 | 80 | 8192 | 102 | 32 | 19.07% | 
| ... | ... | ... | ... | ... | ... | 
| 67 | 32768 | 32768 | 1 | 0 | 12.50% | 
mcache
- mcache 是每个 P 独有的缓存,因此交互无锁
- mcache 缓存了 2(noscan维度) * 68(spanClass维度) = 136 个mspan。
- mcache 中还有 微对象分配器,用于处理小于16B对象的内存分配。
源码:
            
            
              go
              
              
            
          
          const numSpanClasses = 136  
type mcache struct {  
    // 微对象分配器相关  
    tiny       uintptr  
    tinyoffset uintptr  
    tinyAllocs uintptr  
      
    // mcache 中缓存的 mspan,每种 spanClass 各一个  
    alloc [numSpanClasses]*mspan  
    
    // ...  
}mcentral
- 每个mcentral一把锁
- 每个mcentral对应一种spanClass,包含2个mspan链表,分别是有空间的和满的。

            
            
              go
              
              
            
          
          type mcentral struct {  
    // 对应的 spanClass  
    spanclass spanClass  
    // 有空位的 mspan 集合,数组长度为 2 是用于抗一轮 GC  
    partial [2]spanSet   
    // 无空位的 mspan 集合  
    full    [2]spanSet   
}mheap
- 是OS虚拟内存的抽象,向虚拟内存申请页面。
- mheap中页大小为8KB,负责把连续的页组装成mspan。
- heapArena记录了page到mspan的映射关系,一个heapArena包含8192个页,总大小是8192 * 8KB = 64M,这也是mheap向虚拟内存申请内存的基本单位。
- mheap通过类似bitmap的方式,寻找空闲的页,组装成mspan,bit=1表示已组装,bit=0表示空闲。但实际上是通过「基数树」快速定位空闲页面的。
- mheap是mcentral的持有者,缓存了每种等级的mcentral。
源码:
mheap
            
            
              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  
    }  
  
  
    // ...  
}heapArena
            
            
              go
              
              
            
          
          const pagesPerArena = 8192  
  
type heapArena struct {  
    // ...  
    // 实现 page 到 mspan 的映射  
    spans [pagesPerArena]*mspan  
  
  
    // ...  
}