返回介绍

上卷 程序设计

中卷 标准库

下卷 运行时

源码剖析

附录

2.2.1 地址空间

发布于 2024-10-12 19:16:02 字数 10360 浏览 0 评论 0 收藏 0

采取预保留地址空间来获取连续地址空间,以便后续合并内存块,减少碎片。

涉及三个基本概念:

  1. arena : 预保留地址空间,用于分配用户对象。
  2. bitmap : 基于类型信息,以位图标记用户对象指针。(GC)
  3. spans : 反查内存地址所属 mspan 对象。

从 1.11 开始,使用稀疏堆(sparse heap)替代原先超大地址空间的做法。

消除了 512GB 限制,最大可以到 256TB。

相比 1.10 在初始化(mallocinit)阶段预保留(sysReserve),

当前只在分配(sysAlloc)阶段保留,默认只记录地址,如此让进程初始 VIRT 小很多。

// mheap.go

type mheap struct {
    arenaHints *arenaHint
    arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
}
  +--------------+
  | heap         |
  +--------------+            +-----------+            +-----------+
  |   arenaHints | ---------> | arenaHint | ----//---> | arenaHint |
  +--------------+            +-----------+            +-----------+
  |   arenas[L1] | ----+
  +--------------+     |      +----------------+
                       +----> | *heapArena[L2] |
                       |      +----------------+       +-----------+
                       |      | ...            | ----> | heapArena |
                       |      +-------//-------+       +-----------+
                       |      | ...            |       |   bitmap  |
                       |      +----------------+       +-----------+
                       |                               |   spans   |
                       +----> ...                      +-----------+

arenaHints 保存预保留地址空间(arena)的起始地址(addr)和左右方向(down)信息。

初始化时,选择多个易记地址,尝试保留。

如果全部失败,则向操作系统申请,获取可用地址。随后以此为基准,向左或向右连续分配。

如果分配再次失败,则再次向操作系统获取起始地址。如此,就构成了一个或多个连续空间。

heapArena 存储已分配内存块反查表(spans),以及与之对应的指针位图(bitmap)。

将内存地址分解成 L1、L2,以此定位在数组(heap.arenas)内的索引位置。

注意,heapArena.bitmap 记录对象指针信息,垃圾标记结果使用 span.gcmarkBits 位图。

// mheap.go

type arenaHint struct {
    addr uintptr
    down bool
    next *arenaHint
}

type heapArena struct {
    bitmap [heapArenaBitmapBytes]byte
    spans  [pagesPerArena]*mspan
}

mallocinit

内存分配器初始化在 schedinit 内调用,主要设定初始保留地址段。

提前以循环方式预定多个 “固定” 地址。

// malloc.go

func mallocinit() {
    
    // 初始化堆。
    mheap_.init()
    
    // 64-bit
    if sys.PtrSize == 8 {
        
        // 1. 从中间选择多个起始地址,更容易连续扩展。
        // 2. 调试时,这类地址更易识别。
        for i := 0x7f; i >= 0; i-- {
            var p uintptr
            
            switch {
            case GOARCH == "arm64" && GOOS == "darwin":
                p = uintptr(i)<<40 | uintptrMask&(0x0013<<28)
            case GOARCH == "arm64":
                p = uintptr(i)<<40 | uintptrMask&(0x0040<<32)
            default:
                p = uintptr(i)<<40 | uintptrMask&(0x00c0<<32)
            }
            
            // 设定初始保留地址,保存到 mheap.arenaHints 链。
            hint := (*arenaHint)(mheap_.arenaHintAlloc.alloc())
            hint.addr = p
            hint.next, mheap_.arenaHints = mheap_.arenaHints, hint
        }
    } else {
        // 32-bit
        mheap_.arena.init(uintptr(a), size)
    }
}

arenaHint

使用 arenaHints 存储待分配地址。

// mheap.go

type arenaHint struct {
    addr uintptr                   // 递进的分配起始地址,相当于 arena_used。
    down bool                      // 分配方向: true 向低地址,false 向高地址。
    next *arenaHint
}

type mheap struct {
    arenaHints      *arenaHint                                     
    arenaHintAlloc  fixalloc       // hint 使用固定分配器。
}

从 heap.arenaHints 链表提取 arenaHint,获取 addr 和 down 信息,定位分配地址。

如失败,由操作系统提供新可用地址,并将其左右两侧分别构建成 arenaHint 存入链表待用。

// malloc.go

func (h *mheap) sysAlloc(n uintptr) (v unsafe.Pointer, size uintptr) {
    
    // 仅用于 32-bit 分配。
    // heap.arena 在 mallocinit 32-bit 块内初始化。
    // 如果当前 64-bit,那么 h.arena.alloc 返回 nil。
    v = h.arena.alloc(n, heapArenaBytes, &memstats.heap_sys)
    if v != nil {
        goto mapped
    }
    
    // 尝试在 hint.addr 分配。
    for h.arenaHints != nil {
        
        // 从链表头获取 arenaHint,提取保留开始地址。
        hint := h.arenaHints
        p := hint.addr
        
        // 如果向低分配,那么重新计算开始地址。
        if hint.down {
            p -= n
        }
        
        // 尝试保留地址空间。
        v = sysReserve(unsafe.Pointer(p), n)
        
        // 保留成功(地址相符)。
        if p == uintptr(v) {
            // 如果向高扩张,那么更新 hint 内的起始地址。
            // 低扩张记录尾部地址,调整在前面已经完成(p -= n)。
            if !hint.down {
                p += n
            }
            hint.addr = p
            size = n
            
            // 成功后跳出循环。
            break
        }
        
        // 保留失败(对比上一 if 语句,目标地址不符),释放此次所保留空间。
        if v != nil {
            sysFree(v, n, nil)
        }
        
        // 从链表中提取下一个 hint 重试。
        // 释放当前失败的 hint。
        h.arenaHints = hint.next
        h.arenaHintAlloc.free(unsafe.Pointer(hint))
    }
    
    // 依旧没有成功(保留成功才会对 size 赋值)。
    // 表明现有 arenaHints 已没法用了,重新弄一个。
    if size == 0 {
        
        // 由操作系统分配一个可用地址。
        v, size = sysReserveAligned(nil, n, heapArenaBytes)
        if v == nil {
            return nil, 0
        }
        
        // 鉴于操作系统选了一块风水宝地,除当前分配的这块,其左右两边自然是好位置。
        // 将左边(v down)和右边(v + size)的空间分别创建 hints 放入链表头部。
        
        // 左侧(down = true,向低位分配)。
        hint := (*arenaHint)(h.arenaHintAlloc.alloc())
        hint.addr, hint.down = uintptr(v), true           
        hint.next, mheap_.arenaHints = mheap_.arenaHints, hint
        
        // 右侧。
        hint = (*arenaHint)(h.arenaHintAlloc.alloc())
        hint.addr = uintptr(v) + size                     
        hint.next, mheap_.arenaHints = mheap_.arenaHints, hint
    }
    
    // 分配内存
    sysMap(v, size, &memstats.heap_sys)
    
mapped:
    ...
    return
}

heapArena

用二维数组管理多个 heapArena,对应一或多块内存。

通过分解内存地址,获取 L1、L2,在数组中定位。

// mheap.go 

ai := arenaIndex(v)                 // arena index
ha := h.arenas[ai.l1()][ai.l2()]    // heapArena
       heap.arenas
       +-------+-------------//--------------------+
    L1 |  ptr  | ...                               |                   
       +---|---+-------------//--------------------+
           |
           |
           v
       +--------+
    L2 |   ...  |
       +--------+         +-------------------+
       |   ptr -|-------->| heapArena         |
       +--------+         +-------------------+
       |   nil  |         |   bitmap []byte   |
       +--------+         +-------------------+        +-------+
       |   nil  |         |   spans  []*mspan |------->| mspan |-------> {memory}
       +--------+         +-------------------+        +-------+
       |   nil  |
       +--------+
       |   ...  |
       +--------+
       []*HeapArean

在 linux/amd64 平台,每个 heapArena 管理 64MB 内存。

// malloc.go

//       Platform  Addr bits  Arena size  L1 entries   L2 entries
// --------------  ---------  ----------  ----------  -----------
//       */64-bit         48        64MB           1    4M (32MB)
// windows/64-bit         48         4MB          64    1M  (8MB)
//       */32-bit         32         4MB           1  1024  (4KB)
//     */mips(le)         31         4MB           1   512  (2KB)

超出 64MB 的内存块(span)会存储到多个 heapArena 里。

同理,单个 heapArena 也可存储多个地址连续,总容量小于或等于 64MB 的内存块。

位图及反查表大小计算。

// mbitmap.go

// Heap bitmap
//
// The heap bitmap comprises 2 bits for each pointer-sized word in the heap, 
// stored in the heapArena metadata backing each heap arena.
> bitmap size:
    > bits = 64MB / PtrSize * 2bit
    > bytes = bits / 8
// malloc.go

heapArenaBytes = 1 << logHeapArenaBytes                        // 67108864, 64MB
pagesPerArena = heapArenaBytes / pageSize                      // 67108864 / 8192 = 8192
heapArenaBitmapBytes = heapArenaBytes / (sys.PtrSize * 8 / 2)  // 2097152
// mheap.go

type heapArena struct {
    bitmap [heapArenaBitmapBytes]byte   // 2097152
    spans  [pagesPerArena]*mspan        // 8192
}

以 linux/amd64 为例:

L1 长度为 1。

L2 可容纳 4MB = 4194304 个 heapArena 指针。

每个 heapArena 管理 64MB,总容量可达 1 * 4194304 * 64MB = 256TB

ADM64 实际使用 48 位地址总线,所以其上限就是 256TB。

理论上能覆盖地址空间,没有分配区域为 nil。

向操作系统申请内存时(64MB 对齐,倍数),创建相应 heapArena 对象待用。

// malloc.go

func (h *mheap) sysAlloc(n uintptr) (v unsafe.Pointer, size uintptr) {
    
    // 按 64MB 对齐。
    n = alignUp(n, heapArenaBytes)
    
    // 通过 areanHints 确定起始地址 ...
    v = sysReserve(unsafe.Pointer(p), n)
    size = n
    
    // 分配内存。
    sysMap(v, size, &memstats.heap_sys)
    
mapped:
    
    // 根据地址范围,创建对应 heapArean,以便后续保存元数据。
    // 如果内存块大小超标,则需要在多个 L2 位置创建并存储。
    for ri := arenaIndex(uintptr(v)); ri <= arenaIndex(uintptr(v)+size-1); ri++ {
        
        // 基于内存地址(v)计算存储位置。
        l2 := h.arenas[ri.l1()]
        
        // 按需创建 L2 指针数组。
        if l2 == nil {
            l2 = (*[1 << arenaL2Bits]*heapArena)(persistentalloc(unsafe.Sizeof(*l2), sys.PtrSize, nil))
            atomic.StorepNoWB(unsafe.Pointer(&h.arenas[ri.l1()]), unsafe.Pointer(l2))
        }
        
        // 创建 heapArena。
        var r *heapArena
        r = (*heapArena)(h.heapArenaAlloc.alloc(unsafe.Sizeof(*r), sys.PtrSize, &memstats.gc_sys))
        
        // 保存到 L2 指定位置。
        atomic.StorepNoWB(unsafe.Pointer(&l2[ri.l2()]), unsafe.Pointer(r))
    }
    
    return
}

后续分配内存(heap.allocSpan)时,调用 setSpans 填充信息。

按页将 span 指针填充到 heapArena.spans 数组。

大于 64MB 的 span 跨多个 heapArean 存储。

// mheap.go

func (h *mheap) setSpans(base, npage uintptr, s *mspan) {
    p := base / pageSize
    
    // 计算 L1、L2 位置,获取目标 heapArena 对象。
    ai := arenaIndex(base)
    ha := h.arenas[ai.l1()][ai.l2()]
    
    // 按页在一个或多个 heapArena.spans 内填充指针。
    for n := uintptr(0); n < npage; n++ {
        i := (p + n) % pagesPerArena
        
        // 超出,重新计算位置。
        if i == 0 {
            ai = arenaIndex(base + n*pageSize)
            ha = h.arenas[ai.l1()][ai.l2()]
        }
        
        ha.spans[i] = s
    }
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文