上卷 程序设计
中卷 标准库
- bufio 1.18
- bytes 1.18
- io 1.18
- container 1.18
- encoding 1.18
- crypto 1.18
- hash 1.18
- index 1.18
- sort 1.18
- context 1.18
- database 1.18
- connection
- query
- queryrow
- exec
- prepare
- transaction
- scan & null
- context
- tcp
- udp
- http
- server
- handler
- client
- h2、tls
- url
- rpc
- exec
- signal
- embed 1.18
- plugin 1.18
- reflect 1.18
- runtime 1.18
- KeepAlived
- ReadMemStats
- SetFinalizer
- Stack
- sync 1.18
- atomic
- mutex
- rwmutex
- waitgroup
- cond
- once
- map
- pool
- copycheck
- nocopy
- unsafe 1.18
- fmt 1.18
- log 1.18
- math 1.18
- time 1.18
- timer
下卷 运行时
源码剖析
附录
2.2.1 地址空间
采取预保留地址空间来获取连续地址空间,以便后续合并内存块,减少碎片。
涉及三个基本概念:
- arena : 预保留地址空间,用于分配用户对象。
- bitmap : 基于类型信息,以位图标记用户对象指针。(GC)
- 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论