Go语言源码剖析——内存管理概览

模块概述

职责定义

Go的内存管理系统负责为程序分配和回收内存,是运行时系统的核心组件之一。内存分配器基于TCMalloc设计,采用多级缓存策略实现高效的内存分配。其核心目标是:快速分配、减少碎片、支持并发、与垃圾回收器协同工作。

设计理念

分级缓存架构

  • mcache(P级缓存):每个P独占,无锁分配
  • mcentral(中心缓存):按size class管理,所有P共享
  • mheap(堆):全局页分配器,管理所有内存

尺寸分类(Size Class)

  • 将对象大小分为约70个类别
  • 同一类别的对象使用同一个span
  • 减少内存碎片,提高分配效率

内存布局

  • 以页(8KB)为基本单位
  • Span:连续的页组成的内存块
  • Arena:64MB的大块内存,包含多个span

输入与输出

输入

  • 对象分配请求:newmake、字面量创建
  • 大小分类:tiny(<16B无指针)、小对象(≤32KB)、大对象(>32KB)
  • GC标记信息:对象可达性、指针位置
  • 操作系统内存:通过mmap/VirtualAlloc申请

输出

  • 内存地址:返回已分配内存的指针
  • 内存统计:堆大小、分配量、GC指标
  • 内存释放:将未使用的span归还给OS
  • 写屏障支持:配合GC的并发标记

上下游依赖

依赖的下游

  • 操作系统:通过系统调用申请/释放内存
  • P(Processor):每个P拥有独立的mcache
  • 垃圾回收器:扫描堆内对象,标记存活对象
  • 调度器:分配G的栈内存

被依赖的上游

  • 所有Go代码:任何对象创建都需要内存分配
  • Channel:channel结构和缓冲区需要分配
  • Map:map的bucket需要分配
  • Slice:底层数组需要分配
  • Interface:iface结构需要分配

生命周期

初始化阶段(mallocinit)

  1. 确定地址空间布局
  2. 初始化mheap全局堆
  3. 为P创建mcache0(引导阶段)
  4. 设置size class和span class映射表
  5. 从OS申请初始arena

运行阶段

  1. 快速路径:从mcache直接分配(无锁)
  2. 慢路径:mcache缺失,从mcentral补充span
  3. 更慢路径:mcentral缺失,从mheap申请页
  4. 最慢路径:mheap不足,向OS申请新arena
  5. 清理回收:GC后归还未使用的内存给OS

模块架构图

flowchart TB
    subgraph "P的mcache 内存分配缓存"
        MCACHE[mcache结构]
        TINY[tiny分配器<br/>微小对象<16B]
        ALLOC[alloc数组<br/>136个span class]
        STACK[stackcache<br/>栈缓存]
        
        MCACHE --> TINY
        MCACHE --> ALLOC
        MCACHE --> STACK
    end
    
    subgraph "mcentral 中心缓存"
        CENTRAL[mcentral数组<br/>按size class索引]
        PARTIAL[partial链表<br/>部分空闲span]
        FULL[full链表<br/>已满span]
        
        CENTRAL --> PARTIAL
        CENTRAL --> FULL
    end
    
    subgraph "mheap 全局堆"
        HEAP[mheap结构]
        PAGES[pages页分配器<br/>radix tree]
        ARENAS[arenas数组<br/>管理64MB块]
        SPANS[allspans<br/>所有span记录]
        
        HEAP --> PAGES
        HEAP --> ARENAS
        HEAP --> SPANS
    end
    
    subgraph "span 内存块"
        SPAN1[mspan<br/>8KB页]
        SPAN2[mspan<br/>16KB页]
        SPAN3[mspan<br/>32KB页]
        BITMAP[allocBits<br/>分配位图]
        
        SPAN1 --> BITMAP
        SPAN2 --> BITMAP
        SPAN3 --> BITMAP
    end
    
    subgraph "操作系统"
        OS[OS内存<br/>mmap/VirtualAlloc]
    end
    
    ALLOC -.span耗尽.-> CENTRAL
    CENTRAL -.span耗尽.-> HEAP
    HEAP -.内存不足.-> OS
    
    HEAP -.返还.-> OS
    CENTRAL -.归还.-> HEAP
    ALLOC -.归还.-> CENTRAL
    
    subgraph "分配路径"
        REQ[分配请求]
        SIZECHECK{对象大小?}
        TINYPATH[tiny路径<br/>≤16B无指针]
        SMALLPATH[small路径<br/>≤32KB]
        LARGEPATH[large路径<br/>>32KB]
        
        REQ --> SIZECHECK
        SIZECHECK -->|tiny| TINYPATH
        SIZECHECK -->|small| SMALLPATH
        SIZECHECK -->|large| LARGEPATH
        
        TINYPATH --> TINY
        SMALLPATH --> ALLOC
        LARGEPATH --> HEAP
    end

架构图说明

三级缓存结构

第一级:mcache(P级缓存)

  • 每个P拥有独立的mcache
  • 无锁快速分配
  • 包含136个span(68个size class × 2种scan/noscan)
  • 包含tiny分配器用于微小对象(<16B且无指针)

第二级:mcentral(中心缓存)

  • 每个size class对应一个mcentral
  • 管理两个链表:partial(部分空闲)和full(已满)
  • P的mcache缺少span时,从mcentral补充
  • 需要加锁,但粒度细(每个size class一个锁)

第三级:mheap(全局堆)

  • 全局唯一的堆管理器
  • 管理所有的页(page)和span
  • mcentral缺少span时,从mheap申请
  • 使用radix tree高效管理大量页
  • 需要全局锁,访问频率最低

对象分类与分配策略

Tiny对象(<16B且无指针)

分配策略:
1. 多个tiny对象合并到一个16B块中
2. 减少内存碎片和分配次数
3. 从P的mcache.tiny分配
4. 例子:小整数、bool、小字符串

Small对象(16B~32KB)

分配策略:
1. 根据对象大小映射到67个size class之一
2. 从mcache对应span的分配位图查找空闲slot
3. span满时从mcentral获取新span
4. 例子:大部分Go对象、小slice、小map

Large对象(>32KB)

分配策略:
1. 直接从mheap分配
2. 不经过mcache和mcentral
3. 分配整数个页(8KB的倍数)
4. 例子:大数组、大slice、大map

Size Class映射表

Go定义了67个size class(实际使用中是68,因为class 0特殊):

Class Object Size Span Size(pages) Objects Waste
1 8B 1 (8KB) 1024 0%
2 16B 1 512 0%
3 24B 1 341 1.56%
4 32B 1 256 0%
32 1024B 1 8 0%
33 1152B 1 7 1.39%
67 32768B 4 (32KB) 1 0%

Span Class = Size Class × 2 + (noscan?1:0)

  • 每个size class分为scan和noscan两种
  • scan:包含指针,需要GC扫描
  • noscan:不包含指针,GC可跳过
  • 共136个span class(68个size × 2)

mspan数据结构

type mspan struct {
    next     *mspan         // 链表指针
    prev     *mspan         // 链表指针
    startAddr uintptr       // span起始地址
    npages    uintptr       // span包含的页数
    
    nelems      uintptr     // span中object数量
    elemsize    uintptr     // object大小
    spanclass   spanClass   // size class and noscan
    
    allocCount  uint16      // 已分配object数量
    allocBits   *gcBits     // 分配位图
    gcmarkBits  *gcBits     // GC标记位图
    
    sweepgen    uint32      // sweep generation
    allocCache  uint64      // allocBits的缓存
    
    // 链表管理
    state       mSpanStateBox  // mSpanInUse等
    needzero    uint8          // 是否需要清零
    divMul      uint32         // 用于快速除法
}

关键字段说明

  • startAddr:span的起始虚拟地址
  • npages:span占用的8KB页数
  • nelems:span可容纳的对象数量
  • elemsize:每个对象的大小
  • allocCount:已分配对象数
  • allocBits:位图标记哪些slot已分配
  • gcmarkBits:GC标记位图
  • allocCache:allocBits的64位缓存,加速查找空闲slot

分配算法

mallocgc()核心流程

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
    // 1. 快速路径:tiny对象
    if size <= maxTinySize && typ.Kind_&kindNoPointers != 0 {
        off := c.tinyoffset
        // 对齐
        if size&7 == 0 {
            off = alignUp(off, 8)
        } else if goarch.PtrSize == 4 && size == 12 {
            off = alignUp(off, 8)
        } else if size&3 == 0 {
            off = alignUp(off, 4)
        } else if size&1 == 0 {
            off = alignUp(off, 2)
        }
        if off+size <= maxTinySize && c.tiny != 0 {
            // 有足够空间,直接分配
            x = unsafe.Pointer(c.tiny + off)
            c.tinyoffset = off + size
            return x
        }
        // 分配新的tiny块
        span = c.alloc[tinySpanClass]
        v := nextFreeFast(span)
        if v == 0 {
            v, span = c.nextFree(tinySpanClass)
        }
        x = unsafe.Pointer(v)
        (*[2]uint64)(x)[0] = 0
        (*[2]uint64)(x)[1] = 0
        c.tiny = uintptr(x)
        c.tinyoffset = size
        return x
    }

    // 2. 小对象路径
    if size <= maxSmallSize {
        if typ.Kind_&kindNoPointers != 0 {
            span = c.alloc[tSpanClass]
        }
        // 从span分配
        v := nextFreeFast(span)
        if v == 0 {
            v, span = c.nextFree(spc)
        }
        x = unsafe.Pointer(v)
        if needzero && span.needzero != 0 {
            memclrNoHeapPointers(x, size)
        }
        return x
    }

    // 3. 大对象路径
    span = c.allocLarge(size, typ)
    x = unsafe.Pointer(span.base())
    return x
}

nextFreeFast()快速分配

func nextFreeFast(s *mspan) gclinkptr {
    theBit := sys.TrailingZeros64(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)
            s.freeindex = freeidx
            s.allocCount++
            return gclinkptr(result*s.elemsize + s.base())
        }
    }
    return 0
}

算法优化点

  • 使用allocCache缓存64个slot的状态,避免每次都读取内存
  • 使用TrailingZeros64指令快速找到第一个空闲slot
  • Tiny分配器减少小对象的内存开销和碎片

内存布局

虚拟地址空间布局(64位)

+------------------+
| Stack            |  栈向下增长
+------------------+
| ↓                |
|                  |
| ↑                |
+------------------+
| Heap             |  堆向上增长
| - arenas         |  64MB对齐的大块
| - spans          |  管理元数据
+------------------+
| .bss/.data       |  全局变量
+------------------+
| .text            |  代码段
+------------------+

Arena布局(64MB块)

每个arena = 64MB = 8192个page(8KB)
arena metadata存储在heapArena结构中:
- bitmap:标记哪些字包含指针
- spans:指向覆盖该区域的mspan

Span与Object布局

mspan结构 (metadata)
+------------------+
| startAddr        | ------+
| npages           |       |
| elemsize         |       |
| allocBits        |       |
+------------------+       |
                           |
实际内存                    |
+------------------+ <-----+
| object 0         |
+------------------+
| object 1         |
+------------------+
| object 2         |
+------------------+
| ...              |
+------------------+

与GC的协同

写屏障支持

  • 分配器需要支持写屏障的开启/关闭
  • mallocgc在GC标记阶段会触发标记辅助(mark assist)
  • 黑色对象分配:新分配的对象标记为黑色(已扫描)

标记位图

  • 每个span有两个位图:allocBits和gcmarkBits
  • allocBits:哪些object已分配
  • gcmarkBits:GC标记哪些object存活
  • GC sweep阶段,gcmarkBits变成新的allocBits

Sweep清扫

  • GC标记完成后,清扫未标记的object
  • 惰性清扫:span在下次分配时才清扫
  • 清扫时释放未使用的span给mcentral

核心算法详解

1. 对象大小到Size Class的映射

// size到class的映射表
var class_to_size = [_NumSizeClasses]uint16{
    0,    8,    16,   24,   32,   48,   64,   80,
    96,   112,  128,  144,  160,  176,  192,  208,
    224,  240,  256,  288,  320,  352,  384,  416,
    448,  480,  512,  576,  640,  704,  768,  896,
    1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304,
    2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144,
    6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880,
    12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760,
    24576, 27264, 28672, 32768,
}

// 快速查找size class
func sizeToClass(size uintptr) uint8 {
    if size > maxSmallSize {
        throw("invalid size")
    }
    if size > 1024-8 {
        return uint8(size_to_class128[(size-1024+127)>>7])
    }
    return uint8(size_to_class8[(size-1+smallSizeDiv-1)/smallSizeDiv])
}

2. mcentral的span管理

type mcentral struct {
    spanclass spanClass    // size class
    partial  [2]spanSet    // 部分空闲的span列表
    full     [2]spanSet    // 已满的span列表
}

// 从mcentral获取span
func (c *mcentral) cacheSpan() *mspan {
    sg := mheap_.sweepgen
retry:
    // 1. 从partial列表获取
    var s *mspan
    if s = c.partial[sg%2].pop(); s != nil {
        goto havespan
    }
    
    // 2. 从full列表获取(已清扫的span)
    for s = c.full[sg%2].pop(); s != nil; s = c.full[sg%2].pop() {
        if s.sweep(false) {
            goto havespan
        }
    }
    
    // 3. 从mheap申请新span
    s = c.grow()
    if s == nil {
        return nil
    }

havespan:
    // 设置span为已缓存状态
    n := int(s.nelems) - int(s.allocCount)
    if n == 0 || s.freeindex == s.nelems {
        throw("span has no free objects")
    }
    return s
}

3. mheap的页分配

type mheap struct {
    lock mutex
    pages pageAlloc       // 页分配器
    
    allspans []*mspan    // 所有span
    arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
    
    central [numSpanClasses]struct {
        mcentral mcentral
        pad      [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
    }
}

// 分配npages页
func (h *mheap) alloc(npages uintptr, spanclass spanClass) *mspan {
    // 1. 从页分配器获取连续页
    base, scav := h.pages.alloc(npages)
    if base == 0 {
        // 向OS申请更多内存
        if !h.grow(npages) {
            return nil
        }
        base, scav = h.pages.alloc(npages)
        if base == 0 {
            throw("grew heap, but no adequate free space found")
        }
    }
    
    // 2. 初始化span
    s := h.allocMSpanLocked()
    s.init(base, npages)
    s.spanclass = spanclass
    
    // 3. 设置arena映射
    h.setSpans(s.base(), npages, s)
    
    return s
}

核心时序图

1. 小对象分配时序图(Tiny/Small Object)

sequenceDiagram
    autonumber
    participant User as 用户代码
    participant Compiler as 编译器
    participant G as Goroutine
    participant P as P(处理器)
    participant mcache as mcache<br/>(P的内存缓存)
    participant mspan as mspan<br/>(当前span)
    participant mcentral as mcentral<br/>(中央缓存)
    
    User->>Compiler: obj := new(T)
    Compiler->>G: mallocgc(size, typ, needzero)
    
    alt size <= 16 byte (Tiny对象)
        G->>P: 获取当前P
        P->>mcache: 访问P.mcache
        mcache->>mcache: 检查tiny块<br/>(tiny, tinyoffset)
        
        alt tiny块有足够空间
            mcache->>mcache: 从tiny块分配<br/>offset += size
            mcache-->>G: 返回tiny+offset
        else tiny块空间不足
            mcache->>mspan: 访问tiny sizeclass的span
            mspan->>mspan: nextFreeFast()<br/>查找空闲对象
            mspan-->>mcache: 返回新tiny块
            mcache->>mcache: 替换tiny块<br/>offset = size
            mcache-->>G: 返回新地址
        end
    else 16 < size <= 32KB (Small对象)
        G->>G: 计算sizeclass<br/>size → sizeclass
        G->>mcache: 获取对应span<br/>alloc[sizeclass]
        mcache->>mspan: 访问mspan
        
        alt span有空闲对象
            mspan->>mspan: nextFreeFast()<br/>快速路径
            alt allocCache非零
                mspan->>mspan: ctz(allocCache)<br/>找到bit位置
                mspan->>mspan: 清除bit<br/>标记已分配
                mspan-->>mcache: 返回对象地址
            else allocCache为零
                mspan->>mspan: nextFree()<br/>慢速路径
                mspan->>mspan: 刷新allocCache<br/>从allocBits加载
                mspan->>mspan: 再次ctz查找
                mspan-->>mcache: 返回对象地址
            end
            mcache-->>G: 返回对象指针
        else span已满
            G->>mcentral: refill(sizeclass)
            mcentral->>mcentral: cacheSpan()<br/>获取新span
            
            alt nonempty列表有span
                mcentral->>mcentral: 从nonempty获取
                mcentral-->>mcache: 返回span
            else 从empty列表获取
                mcentral->>mcentral: sweep并获取
                mcentral-->>mcache: 返回span
            else 都无可用span
                mcentral->>mcentral: grow(sizeclass)
                mcentral->>mcentral: 向mheap申请<br/>alloc(npages)
                mcentral->>mspan: 切分新span
                mcentral-->>mcache: 返回新span
            end
            
            mcache->>mcache: 替换原span<br/>alloc[sizeclass] = newSpan
            mcache->>mspan: nextFreeFast()
            mspan-->>mcache: 返回对象地址
            mcache-->>G: 返回对象指针
        end
    end
    
    G->>G: 零值初始化(如需)
    G-->>User: 返回对象指针

时序说明

  1. 步骤1-3:编译器转换与入口调用

    • 编译器将new(T)转换为runtime.newobject(typ)
    • newobject内部调用mallocgc(typ.size, typ, true)
    • true表示需要零值初始化
  2. 步骤4-12:Tiny对象分配路径(≤16字节)

    • Tiny块机制:mcache维护一个16字节的tiny块和offset
    • 按8字节对齐将多个小对象合并到同一个tiny块
    • 优势:减少内存碎片,提高缓存利用率
    • 检查顺序
      1. 当前tiny块是否有足够空间(16 - tinyoffset >= size
      2. 若有,直接分配并更新offset
      3. 若无,从tiny sizeclass(通常是class 1)分配新16字节块
    • 对齐处理:根据对象大小调整offset(8字节对齐)
  3. 步骤13-26:Small对象分配快速路径(16B - 32KB)

    • Sizeclass计算:通过查表将size映射到67个sizeclass之一
      sizeclass := size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]
      
    • mcache查找:每个P的mcache有67*2=134个mspan槽位(scan/noscan)
    • nextFreeFast()快速路径
      • allocCache:64位bitmap缓存,每位表示一个对象的分配状态
      • ctz(allocCache):count trailing zeros,找到第一个0位(空闲对象)
      • 时间复杂度:O(1),使用CPU的TZCNT指令
    • nextFree()慢速路径
      • allocCache为全1时触发,从allocBits加载下一个64位
      • allocBits是完整的bitmap,记录span中所有对象状态
  4. 步骤27-42:Span满时从mcentral refill

    • refill(sizeclass)流程
      • 将当前满的span归还给mcentral的empty列表
      • 从mcentral的nonempty列表获取新span
    • cacheSpan()查找策略
      1. nonempty列表:有空闲对象的span(优先)
      2. empty列表:通过sweep找到可回收对象
      3. grow:向mheap申请新内存并切分
    • 加锁粒度:仅在访问mcentral时加锁,每个sizeclass有独立锁
  5. 步骤43-45:零值初始化与返回

    • 根据needzero参数决定是否清零
    • 小对象通常清零(new和make),编译器优化可能省略
    • 返回对象地址,类型指针存储在span.elemtype

边界与性能

  • Tiny对象开销:~10-20ns(无竞争,缓存命中)
  • Small对象开销
    • 快速路径:~20-50ns(mcache命中)
    • 慢速路径:~200-500ns(mcentral refill)
    • 最慢路径:~5-20µs(mheap grow + 切分)
  • 并发性:mcache无锁,每个P独立分配
  • 碎片率:tiny对象合并减少碎片到<5%

异常与回退

  • OOM处理:mheap无法分配时触发runtime.throw("out of memory")
  • GC触发:分配达到阈值时触发GC(gcTrigger.test()
  • 自动扩容sysReserve动态向OS申请内存

2. 大对象分配时序图(Large Object >32KB)

sequenceDiagram
    autonumber
    participant User as 用户代码
    participant G as Goroutine
    participant mheap as mheap<br/>(全局堆)
    participant PageCache as PageCache<br/>(页缓存)
    participant Arena as Arena<br/>(内存区域)
    participant OS as 操作系统
    
    User->>G: obj := make([]T, largeN)
    G->>G: mallocgc(size, typ, false)
    G->>G: 判断size > 32KB
    G->>mheap: alloc(npages, sizeclass=0)
    
    mheap->>mheap: 加全局锁<br/>lock(&mheap.lock)
    
    alt 1. 尝试PageCache
        mheap->>PageCache: 查找npages页<br/>(P本地页缓存)
        
        alt PageCache命中
            PageCache->>PageCache: 取出连续页
            PageCache-->>mheap: 返回页地址
            mheap->>mheap: 初始化span<br/>(sizeclass=0, npages)
            mheap-->>mheap: 解锁
            mheap-->>G: 返回span
        end
    end
    
    alt 2. 尝试free tree查找
        mheap->>mheap: 查找free tree<br/>bestFit(npages)
        
        alt 找到合适span
            mheap->>mheap: 从tree移除span
            
            alt span大小匹配
                mheap->>mheap: 直接使用
                mheap-->>mheap: 解锁
                mheap-->>G: 返回span
            else span过大
                mheap->>mheap: 切分span<br/>split(span, npages)
                mheap->>mheap: 剩余部分放回tree
                mheap-->>mheap: 解锁
                mheap-->>G: 返回span
            end
        end
    end
    
    alt 3. 从Arena申请新内存
        mheap->>mheap: grow(npages)
        mheap->>Arena: sysReserve(size)
        Arena->>OS: mmap(PROT_NONE)
        OS-->>Arena: 返回虚拟地址
        Arena->>OS: madvise(MADV_NOHUGEPAGE)
        OS-->>Arena: 配置页表
        Arena-->>mheap: 返回新区域
        
        mheap->>mheap: 创建新span
        mheap->>mheap: 更新arena映射<br/>setSpans()
        mheap-->>mheap: 解锁
        mheap-->>G: 返回新span
    end
    
    G->>G: 设置span.sizeclass=0<br/>span.elemsize=size
    G-->>User: 返回对象指针

时序说明

  1. 步骤1-4:大对象判定与入口

    • 大对象阈值:size > 32KB
    • 跳过mcache和mcentral,直接从mheap分配
    • 计算所需页数:npages = (size + pageSize - 1) / pageSize
    • sizeclass=0标识大对象
  2. 步骤5-12:尝试从PageCache快速分配

    • PageCache:每个P维护的页缓存,存储最近释放的页
    • 缓存大小:通常64KB,约8页
    • 无锁访问:仅访问P本地数据,无竞争
    • 命中优势:避免全局mheap.lock竞争
  3. 步骤13-26:从free tree查找合适span

    • Treap结构:mheap维护的空闲span索引树
      • 按span大小组织(heap性质)
      • 按地址哈希随机化(tree性质)
      • 支持O(log n)查找和插入
    • bestFit查找
      • 查找≥npages的最小span
      • 减少内存浪费
    • 切分逻辑
      • 若找到的span过大,切分为两部分
      • 使用前npages,剩余部分放回tree
      • 切分后的span独立管理,GC单独标记
  4. 步骤27-40:从OS申请新内存(grow)

    • grow(npages)触发条件
      • PageCache和free tree都无可用内存
      • 向OS申请至少1MB(或npages * pageSize,取较大者)
    • sysReserve()系统调用
      • Linux:mmap(PROT_NONE)预留地址空间
      • 延迟物理内存分配(lazy commit)
      • 避免透明大页(THP)影响性能
    • arena映射更新
      • h.arenas记录所有已分配区域
      • h.spans维护地址到span的映射
      • setSpans(base, npages, s)建立索引
  5. 步骤41-42:初始化span元数据

    • span.sizeclass = 0:标识大对象
    • span.elemsize = size:记录对象实际大小
    • span.nelems = 1:大对象独占span
    • span.allocCount = 1:已分配计数

边界与性能

  • 分配开销
    • PageCache命中:~100-200ns(无锁)
    • Free tree查找:~500ns-2µs(全局锁竞争)
    • OS grow:~10-50µs(系统调用 + 页表设置)
  • 锁竞争:mheap.lock是全局锁,高并发时可能成为瓶颈
  • 内存利用率:bestFit算法减少外部碎片
  • 虚拟内存:预留但未commit,减少物理内存压力

异常与回退

  • OOMsysReserve失败时抛出panic
  • 碎片整理:GC sweep阶段合并相邻空闲span
  • 地址空间耗尽:64位系统几乎不会发生,32位需注意

3. mcentral refill与sweep时序图

sequenceDiagram
    autonumber
    participant mcache as mcache<br/>(P的缓存)
    participant mcentral as mcentral<br/>(中央缓存)
    participant nonempty as nonempty<br/>列表
    participant empty as empty<br/>列表
    participant span1 as Span1<br/>(被sweep)
    participant mheap as mheap<br/>(全局堆)
    
    mcache->>mcentral: refill(sizeclass)
    mcentral->>mcentral: lock(&c.lock)
    
    alt 1. 从nonempty获取
        mcentral->>nonempty: 遍历nonempty列表
        
        alt 找到有空闲对象的span
            nonempty->>mcentral: 返回span
            mcentral->>mcentral: 从列表移除
            mcentral-->>mcentral: unlock
            mcentral-->>mcache: 返回span
        end
    end
    
    alt 2. 从empty列表sweep
        mcentral->>empty: 遍历empty列表
        
        loop 扫描empty列表
            mcentral->>span1: 检查span状态
            
            alt span未sweep
                mcentral->>span1: sweep(s, true)
                span1->>span1: 扫描allocBits<br/>查找可回收对象
                
                alt 找到可回收对象
                    span1->>span1: 重置allocCount<br/>刷新allocCache
                    span1-->>mcentral: 返回span
                    mcentral->>mcentral: 从empty移除
                    mcentral-->>mcentral: unlock
                    mcentral-->>mcache: 返回span
                else 完全空闲
                    span1-->>mcentral: 标记可释放
                    mcentral->>mcentral: 继续扫描
                else 完全满
                    mcentral->>mcentral: 保留在empty
                end
            end
        end
    end
    
    alt 3. 向mheap申请新span
        mcentral->>mcentral: grow(sizeclass)
        mcentral->>mcentral: unlock暂时释放锁
        
        mcentral->>mheap: alloc(npages)
        mheap->>mheap: 分配连续页<br/>(前述大对象流程)
        mheap-->>mcentral: 返回新span
        
        mcentral->>mcentral: lock重新获取锁
        mcentral->>span1: 初始化新span
        span1->>span1: 设置sizeclass<br/>nelems, elemsize
        span1->>span1: 初始化allocBits<br/>全0(全空闲)
        
        mcentral->>nonempty: 将新span加入nonempty
        mcentral-->>mcentral: unlock
        mcentral-->>mcache: 返回新span
    end

时序说明

  1. 步骤1-9:从nonempty列表快速获取

    • nonempty列表:包含至少一个空闲对象的span
    • 遍历策略:从列表头开始,取第一个可用span
    • 移除逻辑:span被mcache获取后从列表移除
    • 归还时机:mcache释放span时,根据空闲对象数量决定放入nonempty或empty
  2. 步骤10-28:从empty列表sweep回收

    • empty列表:可能完全满或等待sweep的span
    • Sweep过程
      • 扫描allocBitsgcmarkBits对比
      • GC标记过但未在allocBits的对象可回收
      • 更新allocCount(已分配对象数)
      • 刷新allocCache为span前64个对象的状态
    • 三种结果
      1. 有空闲对象:移到nonempty并返回
      2. 完全空闲:归还给mheap(freeSpan
      3. 完全满:保留在empty,等待未来回收
  3. 步骤29-44:向mheap申请新span

    • grow(sizeclass)触发:nonempty和empty都无可用span
    • 页数计算:根据sizeclass决定span大小
      npages := class_to_allocnpages[sizeclass]
      
    • 临时释放锁:调用mheap.alloc时释放mcentral锁,避免长时间持锁
    • 初始化新span
      • 设置span.sizeclass, span.nelems, span.elemsize
      • 初始化allocBits为全0(所有对象空闲)
      • 计算allocCache为前64位的镜像
    • 加入nonempty:新span有满span的空闲对象,放入nonempty列表

边界与性能

  • 锁粒度:每个sizeclass独立的mcentral.lock,减少竞争
  • Sweep延迟:lazy sweep策略,仅在需要时sweep
  • 批量操作:一次refill返回一个完整span(约几十到几百个对象)
  • 缓存效果:mcache缓存减少refill频率

异常与回退

  • Sweep失败:所有span都满时触发grow
  • 内存压力:GC触发更积极的sweep
  • Span合并:完全空闲的span归还mheap,可能与相邻span合并

性能优化要点

  • 双列表设计:nonempty和empty分离,加速查找
  • Lazy sweep:延迟清理到实际需要时,分摊开销
  • 批量refill:一次获取整个span,减少mcentral访问次数

性能优化要点

1. 无锁快速路径

  • mcache属于P,分配时无需加锁
  • nextFreeFast使用CPU的TrailingZeros指令
  • 使用allocCache缓存避免内存访问

2. 批量操作

  • mcache从mcentral获取span时,一次获取整个span
  • mcentral从mheap获取页时,一次分配多个页
  • 减少锁的获取次数

3. NUMA感知

  • 尝试从本地NUMA节点分配内存
  • 减少跨节点内存访问延迟

4. 内存复用

  • G结构、defer结构、sudog结构都使用缓存池
  • span在释放后加入free列表,避免重新分配

5. Huge Page支持

  • 在支持的平台使用透明大页(Transparent Huge Pages)
  • 减少TLB miss

最佳实践

1. 减少小对象分配

// 不推荐:频繁分配小对象
for i := 0; i < 1000000; i++ {
    s := fmt.Sprintf("%d", i)  // 每次分配新字符串
    process(s)
}

// 推荐:复用buffer
var buf bytes.Buffer
for i := 0; i < 1000000; i++ {
    buf.Reset()
    fmt.Fprintf(&buf, "%d", i)
    process(buf.String())
}

2. 使用sync.Pool

var bufferPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}

func process() {
    buf := bufferPool.Get().(*bytes.Buffer)
    defer bufferPool.Put(buf)
    buf.Reset()
    // 使用buf
}

3. 预分配slice

// 不推荐:append导致多次分配
var data []int
for i := 0; i < 10000; i++ {
    data = append(data, i)
}

// 推荐:预分配容量
data := make([]int, 0, 10000)
for i := 0; i < 10000; i++ {
    data = append(data, i)
}