go从零单排之内存分配

0 阅读4分钟

Go 原子内存分配


一、Go 内存分配到底是什么?

Go 内存分配 = 线程缓存 + 中心缓存 + 堆管理 + 虚拟内存布局

目标:高并发、无锁 / 轻锁、少碎片、极快分配

三大核心组件

  1. mcacheP 私有缓存(无锁,最快)
  1. mcentral全局中心缓存(每个规格一个,加锁)
  1. mheap堆总控(管理所有内存)

分配规则

  • 小对象(0~16B) :用 mcache 槽位(tiny) 分配
  • 中小对象(16B~32KB) :用 mcache 规格化块(span) 分配
  • 大对象(>32KB) :直接走 mheap 分配

二、Go 内存布局(虚拟地址空间)

Go 把虚拟内存分成 4 个核心区域:

  1. arena:真正的堆内存(最大)
  1. bitmap:标记指针 / GC 信息
  1. spans:记录每个地址属于哪个 span
  1. stacks:栈内存

三、核心结构体

文件:runtime/malloc.go runtime/sizeclasses.go

1. mcache(P 私有,无锁,分配最快)

// mcache 每个 P 一个,私有缓存,无锁
type mcache struct {
    // tiny 分配:小于 16 字节的小对象
    tiny             uintptr    // tiny 分配起始地址
    tinyOffset       uintptr    // tiny 当前偏移量
    tinyAllocs       uintptr    // tiny 分配计数
    // 规格化内存块:共 67 种规格(8B,16B,24B...32KB)
    alloc [numSizeClasses]*mspan // 每种规格一个span链表
}

2. mcentral(全局中心缓存,原子 / 锁操作)

// 每个 size class 一个 mcentral
type mcentral struct {
    sizeclass int32    // 规格ID
    nonempty  mspanList // 有空闲块的 span
    empty     mspanList // 无空闲块的 span
    mutex     mutex     // 保护并发安全
}

3. mspan(内存块最小管理单元)

// span:管理一组连续 pages(8KB)
type mspan struct {
    start       uintptr // 起始地址
    npages      uintptr // 占用页数
    freeIndex   uintptr // 下一个空闲块索引
    freeCount   uintptr // 空闲块数量
    sizeclass   int32   // 规格ID
    state       spanState // 状态:mSpanFree/mSpanInUse
}

4. mheap(全局堆控制器)

var mheap_ mheap
type mheap struct {
    // 所有 mcentral,共67个
    central [numSizeClasses]mcentral
    // 空闲页管理
    freeSpans   [128]mspanList // 按页数分组的空闲span
    // arena 区域
    arena struct {
        start uintptr
        end   uintptr
    }
}

四、内存分配核心流程

1. 分配 tiny 对象(<16B)

  • 直接用 mcache.tiny
  • 多个 tiny 共享一个块
  • 无锁、原子偏移量增加
  • 最快!

2. 分配小对象(16B~32KB)

  1. 计算 size class(67 种规格)
  1. mcache 取对应 span
  1. mcache 有空闲块 → 无锁分配
  1. mcache 没有 → 向 mcentral 申请
  1. mcentral 没有 → 向 mheap 申请
  1. mheap 向操作系统申请内存

3. 分配大对象(>32KB)

  • 直接走 mheap
  • 分配连续页
  • 不进缓存

五、核心分配源码

1. mallocgc 入口

// mallocgc 分配内存
// size: 大小
// typ: 类型
// needZero: 是否清零
func mallocgc(size uintptr, typ *_type, needZero bool) unsafe.Pointer {
    // 1. 超大对象直接走 heap
    if size > maxSmallSize {
        return largeAlloc(size, typ, needZero)
    }
    // 2. tiny 对象分配(<16B)
    if size <= maxTinySize {
        return tinyAlloc(size, typ, needZero)
    }
    // 3. 中小对象(16B~32KB)
    return smallAlloc(size, typ, needZero)
}

2. tinyAlloc 微对象分配(原子无锁)

func tinyAlloc(size uintptr, typ *_type, needZero bool) unsafe.Pointer {
    mp := acquirem()       // 获取当前M
    c := mp.p.ptr().mcache  // 获取P的mcache
    // 原子偏移量 + size
    offset := c.tinyOffset
    newOffset := offset + size
    // 如果当前块足够
    if newOffset <= maxTinySize {
        c.tinyOffset = newOffset // 原子更新偏移
        return unsafe.Pointer(c.tiny + offset)
    }
    // 不够,申请新的tiny块
    return refillTinyCache(size, typ, needZero)
}

3. smallAlloc 小对象分配

func smallAlloc(size uintptr, typ *_type, needZero bool) unsafe.Pointer {
    // 计算 size class
    sc := getSizeClass(size)
    c := getMCache()
    span := c.alloc[sc]
    // 当前 span 有空闲块
    if span.freeCount > 0 {
        // 无锁分配
        ptr := span.start + span.freeIndex*size
        span.freeIndex = *(*uintptr)(unsafe.Pointer(span.start + span.freeIndex))
        span.freeCount--
        return unsafe.Pointer(ptr)
    }
    // mcache 空了,从 mcentral  refill
    c.refill(sc)
    return smallAlloc(size, typ, needZero)
}

4. mcache.refill 从 mcentral 获取 span

func (c *mcache) refill(sc int32) {
    // 加锁,从 mcentral 获取一个有空闲的 span
    span := mheap_.central[sc].mcentral.cacheSpan()
    c.alloc[sc] = span
}

六、Go 内存分配 完整流程图

1. 整体分配主流程

image.png

2. Tiny 微对象分配(最快)

image.png

3. mcache → mcentral → mheap 缓存链

image.png

4. 大对象分配流程

image.png


七、10 个细节

1. mcache 与 P 绑定,无锁,最快

每个 P 自己用自己的缓存,完全无锁

2. tiny 分配是极致优化

多个小对象共享一个块,原子偏移量,无锁。

3. 共 67 种 size class

覆盖 8B ~ 32KB,减少内存碎片

4. mcentral 用锁,但粒度极小

只锁对应规格的 central,并发冲突极低

5. span 是最小管理单元

默认 8KB 一页,一个 span 包含 N 页。

6. mheap 全局唯一

管理所有内存、页、span。

7. 向 OS 申请内存用 mmap

一次性大片申请,减少系统调用

8. 分配时不触发 GC

只有内存达到阈值才触发 GC。

9. 所有本地操作都是原子 / 无锁

保证高并发性能。

10. Go 内存分配是目前业界最快的之一

无锁 + 分层缓存 + 预分配。


八、总结

  • mcache:P 私有,无锁,最快
  • mcentral:全局规格缓存
  • mheap:总控
  • tiny <16B:共享块,原子偏移
  • 16B~32KB:size class 分级管理
  • >32KB:直接走堆
  • 全程无锁 / 轻锁,高并发神器