上卷 程序设计
中卷 标准库
- 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)替代原先超大地址空间的做法。
消除了 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 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论