返回介绍

上卷 程序设计

中卷 标准库

下卷 运行时

源码剖析

附录

2.2.1 地址空间

发布于 2024-10-12 19:15:55 字数 11360 浏览 0 评论 0 收藏 0

通过 预留 方式获取连续地址空间,以便后续合并内存块,减少碎片。

基本概念

  • arena: 预留地址空间,用户对象在此范围内分配。
  • bitmap: 基于类型信息,以位图标记对象指针。(GC)
  • spans: 反查内存归属的 mspan 管理对象。

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

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

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

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

// mheap.go

type mheap struct {
    arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
    arenaHints *arenaHint
}

type heapArena struct {
	// bitmap stores the pointer/scalar bitmap for the words in
	// this arena. See mbitmap.go for a description. 
	bitmap [heapArenaBitmapBytes]byte
    
    // spans maps from virtual address page ID within this arena to *mspan.
	spans [pagesPerArena]*mspan
}

type arenaHint struct {
	addr uintptr
	down bool
	next *arenaHint
}
+--------------+
| heap         |              可分配地址
+--------------+            +-----------+            +-----------+
|   arenaHints | ---------> | arenaHint | ----//---> | arenaHint |
+--------------+            +-----------+            +-----------+
|   arenas[L1] | ----+
+--------------+     |      +----------------+
                     +----> | *heapArena[L2] |         已分配区域
                     |      +----------------+       +-----------+
                     |      | ...            | ----> | heapArena |
                     |      +-------//-------+       +-----------+
                     |      | ...            |       |   bitmap  |  指针位图
                     |      +----------------+       +-----------+
                     |                               |   spans   |  反查表
                     +----> ...                      +-----------+

初始化

前文(1.2 节)看到 schedinit 调用 mallocinit 进行内存分配器初始化。

// mheap.go

var mheap_ mheap
// malloc.go

func mallocinit() {

	// 初始化堆。
	mheap_.init()
	mcache0 = allocmcache()

	// 预留地址空间。
	if goarch.PtrSize == 8 {  // 64-bit
    
		// On a 64-bit machine, we pick the following hints
		// because:
		//
		// 1. Starting from the middle of the address space
		// makes it easier to grow out a contiguous range
		// without running in to some other mapping.
		//
		// 2. This makes Go heap addresses more easily
		// recognizable when debugging.
		//
		// 3. Stack scanning in gccgo is still conservative,
		// so it's important that addresses be distinguishable
		// from other data.
		//
		// Starting at 0x00c0 means that the valid memory addresses
		// will begin 0x00c0, 0x00c1, ...
    
		for i := 0x7f; i >= 0; i-- {
			var p uintptr
      
			switch {
			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, false)
    	...
	}
}

可以看出,初始化时记录了很多段地址空间,依次存储在链表内。

仅记录,而非提前预留(sysReserved),这有助于减少启动时虚拟内存数值。

// malloc.go

553			hint := (*arenaHint)(mheap_.arenaHintAlloc.alloc())
554			hint.addr = p
555			hint.next, mheap_.arenaHints = mheap_.arenaHints, hint
556		}
557	} else {
$ gdb test

(gdb) b malloc.go:555
Breakpoint 1 at 0x40ccaa: file /usr/local/go/src/runtime/malloc.go, line 555.

(gdb) r
Breakpoint 1, runtime.mallocinit () at /usr/local/go/src/runtime/malloc.go:555
555				hint.next, mheap_.arenaHints = mheap_.arenaHints, hint


(gdb) display/x *hint

1: /x *hint = {
  addr = 0x7fc000000000,    ; 0x7fc
  down = 0x0,
  next = 0x0
}

(gdb) c

1: /x *hint = {
  addr = 0x7ec000000000,    ; 0x7ec
  down = 0x0,
  next = 0x0
}

(gdb) c

1: /x *hint = {
  addr = 0x7dc000000000,    ; 0x7dc
  down = 0x0,
  next = 0x0
}

可分配地址

每个 arenaHint 记录可分配起始地址(累进)及分配方向(向高位或地位)。如分配失败,则尝试从链表获取下一个 arenaHint 重试,或由操作系统提供随机地址。

// mheap.go

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

调用 sysAlloc 分配内存时,先获取有效地址。

// malloc.go

// sysAlloc allocates heap arena space for at least n bytes. The
// returned pointer is always heapArenaBytes-aligned and backed by
// h.arenas metadata. 
//
// sysAlloc returns a memory region in the Reserved state. This region must
// be transitioned to Prepared and then Ready before use.

func (h *mheap) sysAlloc(n uintptr) (v unsafe.Pointer, size uintptr) {

	n = alignUp(n, heapArenaBytes)

    // 用于 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 {
		size = n
		goto mapped
	}

	// 尝试用 hint.addr 地址。
	for h.arenaHints != nil {
        
		hint := h.arenaHints
		p := hint.addr
        
        // 如果向低位分配,那么需要调整起始地址。
		if hint.down {
			p -= n
		}
        
        // 保留该地址段。
		if p+n < p {
			// We can't use this, so don't ask.
			v = nil
		} else if arenaIndex(p+n-1) >= 1<<arenaBits {
			// Outside addressable heap. Can't use.
			v = nil
		} else {
			v = sysReserve(unsafe.Pointer(p), n)
		}
        
        // 如果保留成功,更新 hint 数据。
		if p == uintptr(v) {
			// Success. Update the hint.
			if !hint.down {
				p += n
			}
			hint.addr = p
			size = n
			break
		}
        
		// Failed. Discard this hint and try the next.
		if v != nil {
			sysFree(v, n, nil)
		}
        
        // 失败!尝试下一链表项。并释放当前 hint 内存。
		h.arenaHints = hint.next
		h.arenaHintAlloc.free(unsafe.Pointer(hint))
	}

    // 尝试整个链表后依旧失败,则向操作系统申请。
	if size == 0 {
        
		// All of the hints failed, so we'll take any
		// (sufficiently aligned) address the kernel will give
		// us.
        
		v, size = sysReserveAligned(nil, n, heapArenaBytes)
		if v == nil {
			return nil, 0
		}

        // 操作系统返回一个可用地址。
        // 将该地址左右两个区域存储到链表内,以供下次使用。

        // 左侧。
		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
	}

mapped:

	return
}

已分配空间

用数组管理多个 heapArena,每个对应一到多块内存。

分解内存地址,获取 L1、L2 索引,用于定位。

// mheap.go

func (h *mheap) setSpans(base, npage uintptr, s *mspan) {

    ai := arenaIndex(base)               // 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 管理 64 MB 内存。

// 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)
//      ios/arm64         33         4MB           1  2048  (8KB)
//       */32-bit         32         4MB           1  1024  (4KB)
//     */mips(le)         31         4MB           1   512  (2KB)

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

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

位图(bitmap)及反查表(spans)大小计算。

// 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 is the size of a heap arena. The heap
// consists of mappings of size heapArenaBytes, aligned to
// heapArenaBytes. The initial heap mapping is one arena.
//
// This is currently 64MB on 64-bit non-Windows and 4MB on
// 32-bit and on Windows. 
heapArenaBytes = 1 << logHeapArenaBytes

// heapArenaBitmapBytes is the size of each heap arena's bitmap.
heapArenaBitmapBytes = heapArenaBytes / (goarch.PtrSize * 8 / 2)  // 2097152

pagesPerArena = heapArenaBytes / pageSize  // 67108864 / 8192 = 8192
// mheap.go

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

以 linux/amd64 为例:

L1 长度为 1。

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

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

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

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

流程

上层部件(mcentral.grow、largeAlloc)调用 mheap.alloc 从堆获取内存。

// mheap.go

// alloc allocates a new span of npage pages from the GC'd heap.
func (h *mheap) alloc(npages uintptr, spanclass spanClass) *mspan {
	s = h.allocSpan(npages, spanAllocHeap, spanclass)
	return s
}

期间,调用 setSpans 填充反查表。

// mheap.go

func (h *mheap) allocSpan(npages uintptr, typ spanAllocType, spanclass spanClass) (s *mspan) {

    ...
    
    base, scav = h.pages.alloc(npages)
    
HaveSpan:
    
    h.setSpans(s.base(), npages, s)
}
// 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
	}
}

而 heapArena 是在扩张(grow)时调用 sysAlloc 创建。

// malloc.go

// sysAlloc allocates heap arena space for at least n bytes. 
func (h *mheap) sysAlloc(n uintptr) (v unsafe.Pointer, size uintptr) {

	// 按 64MB 对齐。
	n = alignUp(n, heapArenaBytes)

	v = sysReserve(unsafe.Pointer(p), n)
	v, size = sysReserveAligned(nil, n, heapArenaBytes)

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 {
			// Allocate an L2 arena map.
			l2 = (*[1 << arenaL2Bits]*heapArena)(persistentalloc(unsafe.Sizeof(*l2), goarch.PtrSize, nil))
			atomic.StorepNoWB(unsafe.Pointer(&h.arenas[ri.l1()]), unsafe.Pointer(l2))
		}

        // 创建 heapArena,存储指针到 L2 数组。
		var r *heapArena
		r = (*heapArena)(h.heapArenaAlloc.alloc(unsafe.Sizeof(*r), goarch.PtrSize, &memstats.gcMiscSys))
		atomic.StorepNoWB(unsafe.Pointer(&l2[ri.l2()]), unsafe.Pointer(r))
	}

	return
}

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

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

发布评论

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