微分配器
线程缓存中还包含几个用于分配微对象的字段,下面的这三个字段组成了微对象分配器,专门为 16 字节以下的对象申请和管理内存:
Go
type mcache struct { tiny uintptr tinyoffset uintptr local_tinyallocs uintptr}
复制代码
微分配器只会用于分配非指针类型的内存,上述三个字段中 tiny 会指向堆中的一篇内存,tinyOffset 是下一个空闲内存所在的偏移量,最后的 local_tinyallocs 会记录内存分配器中分配的对象个数。
中心缓存
runtime.mcentral 是内存分配器的中心缓存,与线程缓存不同,访问中心缓存中的内存管理单元需要使用互斥锁:
Go
type mcentral struct { lock mutex spanclass spanClass nonempty mSpanList empty mSpanList nmalloc uint64}
复制代码
每一个中心缓存都会管理某个跨度类的内存管理单元,它会同时持有两个 runtime.mSpanList,分别存储包含空闲对象的列表和不包含空闲对象的链表:
图 7-16 中心缓存和内存管理单元
该结构体在初始化时,两个链表都不包含任何内存,程序运行时会扩容结构体持有的两个链表,nmalloc 字段也记录了该结构体中分配的对象个数。
内存管理单元
线程缓存会通过中心缓存的 runtime.mcentral.cacheSpan 方法获取新的内存管理单元,该方法的实现比较复杂,我们可以将其分成以下几个部分:
从非空链表中查找可以使用的内存管理单元;
从空闲链表中查找可以使用的内存管理单元;
调用 runtime.mcentral.grow 从堆中申请新的内存管理单元;
更新内存管理单元的 allocCache 等字段帮助快速分配内存;
首先我们会在中心缓存的非空链表中查找可用的 runtime.mspan,根据 sweepgen 字段分别进行不同的处理:
当内存单元等待回收时,将其插入空闲链表队列、调用 runtime.mspan.sweep 清理该单元并返回;
当内存单元正在被后台回收时,跳过该内存单元;
当内存单元已经被回收时,将内存单元插入空闲链表队列并返回;
Go
func (c *mcentral) cacheSpan() *mspan { sg := mheap_.sweepgenretry: var s *mspan for s = c.nonempty.first; s != nil; s = s.next { if s.sweepgen == sg-2 && atomic.Cas(&s.sweepgen, sg-2, sg-1) { // 等待回收 c.nonempty.remove(s) c.empty.insertBack(s) s.sweep(true) goto havespan } if s.sweepgen == sg-1 { // 正在回收 continue } c.nonempty.remove(s) // 已经回收 c.empty.insertBack(s) goto havespan } ...}
复制代码
如果中心缓存没有在非空链表中找到可用的内存管理单元,就会继续遍历其持有的空闲链表,我们在这里的处理与非空链表几乎完全相同。当找到需要回收的内存单元时,我们也会触发 runtime.mspan.sweep 进行清理,如果清理后的内存单元仍然非空,就会重新执行相应的代码:
Go
func (c *mcentral) cacheSpan() *mspan { ... for s = c.empty.first; s != nil; s = s.next { if s.sweepgen == sg-2 && atomic.Cas(&s.sweepgen, sg-2, sg-1) { c.empty.remove(s) s.sweep(true) freeIndex := s.nextFreeIndex() if freeIndex != s.nelems { s.freeindex = freeIndex goto havespan } goto retry // 不包含空闲对象 } if s.sweepgen == sg-1 { continue } break } ...}
复制代码
如果 runtime.mcentral 在两个链表中都没有找到可用的内存单元,它会调用 runtime.mcentral.grow 触发扩容操作从堆中申请新的内存:
Go
func (c *mcentral) cacheSpan() *mspan { ... s = c.grow() if s == nil { return nil } c.empty.insertBack(s)
havespan: n := int(s.nelems) - int(s.allocCount) atomic.Xadd64(&c.nmalloc, int64(n)) if gcBlackenEnabled != 0 { gcController.revise() } freeByteBase := s.freeindex &^ (64 - 1) whichByte := freeByteBase / 8 s.refillAllocCache(whichByte) s.allocCache >>= s.freeindex % 64
return s}
复制代码
无论通过哪种方法获取到了内存单元,该方法的最后都会对内存单元的 allocBits 和 allocCache 等字段进行更新,让运行时在分配内存时能够快速找到空闲的对象。
扩容
中心缓存的扩容方法 runtime.mcentral.grow 会根据预先计算的 class_to_allocnpages 和 class_to_size 获取待分配的页数以及跨度类并调用 runtime.mheap.alloc 获取新的 runtime.mspan 结构:
Go
func (c *mcentral) grow() *mspan { npages := uintptr(class_to_allocnpages[c.spanclass.sizeclass()]) size := uintptr(class_to_size[c.spanclass.sizeclass()])
s := mheap_.alloc(npages, c.spanclass, true) if s == nil { return nil }
n := (npages << _PageShift) >> s.divShift * uintptr(s.divMul) >> s.divShift2 s.limit = s.base() + size*n heapBitsForAddr(s.base()).initSpan(s) return s}
复制代码
获取了 runtime.mspan 之后,我们会在上述方法中初始化 limit 字段并清除该结构在堆上对应的位图。
页堆
runtime.mheap 是内存分配的核心结构体,Go 语言程序只会存在一个全局的结构,而堆上初始化的所有对象都由该结构体统一管理,该结构体中包含两组非常重要的字段,其中一个是全局的中心缓存列表 central,另一个是管理堆区内存区域的 arenas 以及相关字段。
页堆中包含一个长度为 134 的 runtime.mcentral 数组,其中 67 个为跨度类需要 scan 的中心缓存,另外的 67 个是 noscan 的中心缓存:
图 7-17 页堆与中心缓存列表
我们在设计原理一节中已经介绍过 Go 语言所有的内存空间都由如下所示的二维矩阵 runtime.heapArena 管理的,这个二维矩阵管理的内存可以是不连续的:
图 7-18 页堆管理的内存区域
在除了 Windows 以外的 64 位操作系统中,每一个 runtime.heapArena 都会管理 64MB 的内存空间,如下所示的表格展示了不同平台上 Go 语言程序管理的堆区大小以及 runtime.heapArena 占用的内存空间:
平台 | 地址位数 | Arena 大小 | 一维大小 | 二维大小
复制代码
--------------:| ----:| --------:| ----:| ----------:
*/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)
表 7-3 平台与页堆大小的关系
本节将介绍页堆的初始化、内存分配以及内存管理单元分配的过程,这些过程能够帮助我们理解全局变量页堆与其他组件的关系以及它管理内存的方式。
初始化
堆区的初始化会使用 runtime.mheap.init 方法,我们能看到该方法初始化了非常多的结构体和字段,不过其中初始化的两类变量比较重要:
spanalloc、cachealloc 以及 arenaHintAlloc 等 runtime.fixalloc 类型的空闲链表分配器;
central 切片中 runtime.mcentral 类型的中心缓存;
Go
func (h *mheap) init() { h.spanalloc.init(unsafe.Sizeof(mspan{}), recordspan, unsafe.Pointer(h), &memstats.mspan_sys) h.cachealloc.init(unsafe.Sizeof(mcache{}), nil, nil, &memstats.mcache_sys) h.specialfinalizeralloc.init(unsafe.Sizeof(specialfinalizer{}), nil, nil, &memstats.other_sys) h.specialprofilealloc.init(unsafe.Sizeof(specialprofile{}), nil, nil, &memstats.other_sys) h.arenaHintAlloc.init(unsafe.Sizeof(arenaHint{}), nil, nil, &memstats.other_sys)
h.spanalloc.zero = false
for i := range h.central { h.central[i].mcentral.init(spanClass(i)) }
h.pages.init(&h.lock, &memstats.gc_sys)}
复制代码
堆中初始化的多个空闲链表分配器与我们在设计原理一节中提到的分配器没有太多区别,当我们调用 runtime.fixalloc.init 初始化分配器时,需要传入带初始化的结构体大小等信息,这会帮助分配器分割待分配的内存,该分配器提供了以下两个用于分配和释放内存的方法:
runtime.fixalloc.alloc — 获取下一个空闲的内存空间;
runtime.fixalloc.free — 释放指针指向的内存空间;
除了这些空闲链表分配器之外,我们还会在该方法中初始化所有的中心缓存,这些中心缓存会维护全局的内存管理单元,各个线程会通过中心缓存获取新的内存单元。
内存管理单元
runtime.mheap 是内存分配器中的核心组件,运行时会通过它的 runtime.mheap.alloc 方法在系统栈中获取新的 runtime.mspan:
Go
func (h *mheap) alloc(npages uintptr, spanclass spanClass, needzero bool) *mspan { var s *mspan systemstack(func() { if h.sweepdone == 0 { h.reclaim(npages) } s = h.allocSpan(npages, false, spanclass, &memstats.heap_inuse) }) ... return s}
复制代码
为了阻止内存的大量占用和堆的增长,我们在分配对应页数的内存前需要先调用 runtime.mheap.reclaim 方法回收一部分内存,接下来我们将通过 runtime.mheap.allocSpan 分配新的内存管理单元,我们会将该方法的执行过程拆分成两个部分:
从堆上分配新的内存页和内存管理单元 runtime.mspan;
初始化内存管理单元并将其加入 runtime.mheap 持有内存单元列表;
首先我们需要在堆上申请 npages 数量的内存页并初始化 runtime.mspan:
Go
func (h *mheap) allocSpan(npages uintptr, manual bool, spanclass spanClass, sysStat *uint64) (s *mspan) { gp := getg() base, scav := uintptr(0), uintptr(0) pp := gp.m.p.ptr() if pp != nil && npages < pageCachePages/4 { c := &pp.pcache base, scav = c.alloc(npages) if base != 0 { s = h.tryAllocMSpan() if s != nil && gcBlackenEnabled == 0 && (manual || spanclass.sizeclass() != 0) { goto HaveSpan } } }
if base == 0 { base, scav = h.pages.alloc(npages) if base == 0 { h.grow(npages) base, scav = h.pages.alloc(npages) if base == 0 { throw("grew heap, but no adequate free space found") } } } if s == nil { s = h.allocMSpanLocked() } ...}
复制代码
上述方法会通过处理器的页缓存 runtime.pageCache 或者全局的页分配器 runtime.pageAlloc 两种途径从堆中申请内存:
如果申请的内存比较小,获取申请内存的处理器并尝试调用 runtime.pageCache.alloc 获取内存区域的基地址和大小;
如果申请的内存比较大或者线程的页缓存中内存不足,会通过 runtime.pageAlloc.alloc 在页堆上申请内存;
如果发现页堆上的内存不足,会尝试通过 runtime.mheap.grow 进行扩容并重新调用 runtime.pageAlloc.alloc 申请内存;
如果申请到内存,意味着扩容成功;
如果没有申请到内存,意味着扩容失败,宿主机可能不存在空闲内存,运行时会直接中止当前程序;
无论通过哪种方式获得内存页,我们都会在该函数中分配新的 runtime.mspan 结构体;该方法的剩余部分会通过页数、内存空间以及跨度类等参数初始化它的多个字段:
Go
func (h *mheap) alloc(npages uintptr, spanclass spanClass, needzero bool) *mspan { ...HaveSpan: s.init(base, npages)
...
s.freeindex = 0 s.allocCache = ^uint64(0) s.gcmarkBits = newMarkBits(s.nelems) s.allocBits = newAllocBits(s.nelems) h.setSpans(s.base(), npages, s) return s}
复制代码
在上述代码中,我们通过调用 runtime.mspan.init 方法以及设置参数初始化刚刚分配的 runtime.mspan 结构并通过 runtime.mheaps.setSpans 方法建立页堆与内存单元的联系。
扩容
runtime.mheap.grow 方法会向操作系统申请更多的内存空间,传入的页数经过对齐可以得到期望的内存大小,我们可以将该方法的执行过程分成以下几个部分:
通过传入的页数获取期望分配的内存空间大小以及内存的基地址;
如果 arena 区域没有足够的空间,调用 runtime.mheap.sysAlloc 从操作系统中申请更多的内存;
扩容 runtime.mheap 持有的 arena 区域并更新页分配器的元信息;
在某些场景下,调用 runtime.pageAlloc.scavenge 回收不再使用的空闲内存页;
在页堆扩容的过程中,runtime.mheap.sysAlloc 是页堆用来申请虚拟内存的方法,我们会分几部分介绍该方法的实现。首先,该方法会尝试在预保留的区域申请内存:
Go
func (h *mheap) sysAlloc(n uintptr) (v unsafe.Pointer, size uintptr) { n = alignUp(n, heapArenaBytes)
v = h.arena.alloc(n, heapArenaBytes, &memstats.heap_sys) if v != nil { size = n goto mapped } ...}
复制代码
上述代码会调用线性分配器的 runtime.linearAlloc.alloc 方法在预先保留的内存中申请一块可以使用的空间。如果没有可用的空间,我们会根据页堆的 arenaHints 在目标地址上尝试扩容:
Go
func (h *mheap) sysAlloc(n uintptr) (v unsafe.Pointer, size uintptr) { ... for h.arenaHints != nil { hint := h.arenaHints p := hint.addr v = sysReserve(unsafe.Pointer(p), n) if p == uintptr(v) { hint.addr = p size = n break } h.arenaHints = hint.next h.arenaHintAlloc.free(unsafe.Pointer(hint)) } ... sysMap(v, size, &memstats.heap_sys) ...}
复制代码
runtime.sysReserve 和 runtime.sysMap 是上述代码的核心部分,它们会从操作系统中申请内存并将内存转换至 Prepared 状态。
Go
func (h *mheap) sysAlloc(n uintptr) (v unsafe.Pointer, size uintptr) { ...mapped: for ri := arenaIndex(uintptr(v)); ri <= arenaIndex(uintptr(v)+size-1); ri++ { l2 := h.arenas[ri.l1()] r := (*heapArena)(h.heapArenaAlloc.alloc(unsafe.Sizeof(*r), sys.PtrSize, &memstats.gc_sys)) ... h.allArenas = h.allArenas[:len(h.allArenas)+1] h.allArenas[len(h.allArenas)-1] = ri atomic.StorepNoWB(unsafe.Pointer(&l2[ri.l2()]), unsafe.Pointer(r)) } return}
复制代码
runtime.mheap.sysAlloc 方法在最后会初始化一个新的 runtime.heapArena 结构体来管理刚刚申请的内存空间,该结构体会被加入页堆的二维矩阵中。
7.1.3 内存分配
堆上所有的对象都会通过调用 runtime.newobject 函数分配内存,该函数会调用 runtime.mallocgc 分配指定大小的内存空间,这也是用户程序向堆上申请内存空间的必经函数:
Go
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { mp := acquirem() mp.mallocing = 1
c := gomcache() var x unsafe.Pointer noscan := typ == nil || typ.ptrdata == 0 if size <= maxSmallSize { if noscan && size < maxTinySize { // 微对象分配 } else { // 小对象分配 } } else { // 大对象分配 }
publicationBarrier() mp.mallocing = 0 releasem(mp)
return x}
复制代码
上述代码使用 runtime.gomcache 获取了线程缓存并通过类型判断类型是否为指针类型。我们从这个代码片段可以看出 runtime.mallocgc 会根据对象的大小执行不同的分配逻辑,在前面的章节也曾经介绍过运行时根据对象大小将它们分成微对象、小对象和大对象,这里会根据大小选择不同的分配逻辑:
图 7-19 三种对象
微对象 (0, 16B) — 先使用微型分配器,再依次尝试线程缓存、中心缓存和堆分配内存;
小对象 [16B, 32KB] — 依次尝试使用线程缓存、中心缓存和堆分配内存;
大对象 (32KB, +∞) — 直接在堆上分配内存;
我们会依次介绍运行时分配微对象、小对象和大对象的过程,梳理内存分配的核心执行流程。
微对象
Go 语言运行时将小于 16 字节的对象划分为微对象,它会使用线程缓存上的微分配器提高微对象分配的性能,我们主要使用它来分配较小的字符串以及逃逸的临时变量。微分配器可以将多个较小的内存分配请求合入同一个内存块中,只有当内存块中的所有对象都需要被回收时,整片内存才可能被回收。
微分配器管理的对象不可以是指针类型,管理多个对象的内存块大小 maxTinySize 是可以调整的,在默认情况下,内存块的大小为 16 字节。maxTinySize 的值越大,组合多个对象的可能性就越高,内存浪费也就越严重;maxTinySize 越小,内存浪费就会越少,不过无论如何调整,8 的倍数都是一个很好的选择。
图 7-20 微分配器的工作原理
如上图所示,微分配器已经在 16 字节的内存块中分配了 12 字节的对象,如果下一个待分配的对象小于 4 字节,它就会直接使用上述内存块的剩余部分,减少内存碎片,不过该内存块只有在 3 个对象都被标记为垃圾时才会被回收。
线程缓存 runtime.mcache 中的 tiny 字段指向了 maxTinySize 大小的块,如果当前块中还包含大小合适的空闲内存,运行时会通过基地址和偏移量获取并返回这块内存:
Go
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { ... if size <= maxSmallSize { if noscan && size < maxTinySize { off := c.tinyoffset if off+size <= maxTinySize && c.tiny != 0 { x = unsafe.Pointer(c.tiny + off) c.tinyoffset = off + size c.local_tinyallocs++ releasem(mp) return x } ... } ... } ...}
复制代码
当内存块中不包含空闲的内存时,下面的这段代码会从先线程缓存找到跨度类对应的内存管理单元 runtime.mspan,调用 runtime.nextFreeFast 获取空闲的内存;当不存在空闲内存时,我们会调用 runtime.mcache.nextFree 从中心缓存或者页堆中获取可分配的内存块:
Go
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { ... if size <= maxSmallSize { if noscan && size < maxTinySize { ... span := c.alloc[tinySpanClass] v := nextFreeFast(span) if v == 0 { v, _, _ = c.nextFree(tinySpanClass) } x = unsafe.Pointer(v) (*[2]uint64)(x)[0] = 0 (*[2]uint64)(x)[1] = 0 if size < c.tinyoffset || c.tiny == 0 { c.tiny = uintptr(x) c.tinyoffset = size } size = maxTinySize } ... } ... return x}
复制代码
获取新的空闲内存块之后,上述代码会清空空闲内存中的数据、更新构成微对象分配器的几个字段 tiny 和 tinyoffset 并返回新的空闲内存。
小对象
小对象是指大小为 16 字节到 32,768 字节的对象以及所有小于 16 字节的指针类型的对象,小对象的分配可以被分成以下的三个步骤:
确定分配对象的大小以及跨度类 runtime.spanClass;
从线程缓存、中心缓存或者堆中获取内存管理单元并从内存管理单元找到空闲的内存空间;
调用 runtime.memclrNoHeapPointers 清空空闲内存中的所有数据;
确定待分配的对象大小以及跨度类需要使用预先计算好的 size_to_class8、size_to_class128 以及 class_to_size 字典,这些字典能够帮助我们快速获取对应的值并构建 runtime.spanClass:
Go
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { ... if size <= maxSmallSize { ... } else { var sizeclass uint8 if size <= smallSizeMax-8 { sizeclass = size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv] } else { sizeclass = size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv] } size = uintptr(class_to_size[sizeclass]) spc := makeSpanClass(sizeclass, noscan) span := c.alloc[spc] v := nextFreeFast(span) if v == 0 { v, span, _ = c.nextFree(spc) } x = unsafe.Pointer(v) if needzero && span.needzero != 0 { memclrNoHeapPointers(unsafe.Pointer(v), size) } } } else { ... } ... return x}
复制代码
在上述代码片段中,我们会重点分析两个函数和方法的实现原理,它们分别是 runtime.nextFreeFast 和 runtime.mcache.nextFree,这两个函数会帮助我们获取空闲的内存空间。runtime.nextFreeFast 会利用内存管理单元中的 allocCache 字段,快速找到该字段中位 1 的位数,我们在上面介绍过 1 表示该位对应的内存空间是空闲的:
Go
func nextFreeFast(s *mspan) gclinkptr { theBit := sys.Ctz64(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、freeindex 等字段并返回该片内存了;如果我们没有找到空闲的内存,运行时会通过 runtime.mcache.nextFree 找到新的内存管理单元:
Go
func (c *mcache) nextFree(spc spanClass) (v gclinkptr, s *mspan, shouldhelpgc bool) { s = c.alloc[spc] freeIndex := s.nextFreeIndex() if freeIndex == s.nelems { c.refill(spc) s = c.alloc[spc] freeIndex = s.nextFreeIndex() }
v = gclinkptr(freeIndex*s.elemsize + s.base()) s.allocCount++ return}
复制代码
在上述方法中,如果我们在线程缓存中没有找到可用的内存管理单元,会通过前面介绍的 runtime.mcache.refill 使用中心缓存中的内存管理单元替换已经不存在可用对象的结构体,该方法会调用新结构体的 runtime.mspan.nextFreeIndex 获取空闲的内存并返回。
大对象
运行时对于大于 32KB 的大对象会单独处理,我们不会从线程缓存或者中心缓存中获取内存管理单元,而是直接在系统的栈中调用 runtime.largeAlloc 函数分配大片的内存:
Go
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer { ... if size <= maxSmallSize { ... } else { var s *mspan systemstack(func() { s = largeAlloc(size, needzero, noscan) }) s.freeindex = 1 s.allocCount = 1 x = unsafe.Pointer(s.base()) size = s.elemsize }
publicationBarrier() mp.mallocing = 0 releasem(mp)
return x}
复制代码
runtime.largeAlloc 函数会计算分配该对象所需要的页数,它会按照 8KB 的倍数为对象在堆上申请内存:
Go
func largeAlloc(size uintptr, needzero bool, noscan bool) *mspan { npages := size >> _PageShift if size&_PageMask != 0 { npages++ } ... s := mheap_.alloc(npages, makeSpanClass(0, noscan), needzero) s.limit = s.base() + size heapBitsForAddr(s.base()).initSpan(s) return s}
复制代码
申请内存时会创建一个跨度类为 0 的 runtime.spanClass 并调用 runtime.mheap.alloc 分配一个管理对应内存的管理单元。
7.1.4 小结
内存分配是 Go 语言运行时内存管理的核心逻辑,运行时的内存分配器使用类似 TCMalloc 的分配策略将对象根据大小分类,并设计多层级的组件提高内存分配器的性能。本节不仅介绍了 Go 语言内存分配器的设计与实现原理,同时也介绍了内存分配器的常见设计,帮助我们理解不同编程语言在设计内存分配器时做出的不同选择。
内存分配器虽然非常重要,但是它只解决了如何分配内存的问题,我们在本节中省略了很多与垃圾回收相关的代码,没有分析运行时垃圾回收的实现原理,在下一节中我们将详细分析 Go 语言垃圾回收的设计与实现原理。
7.1.5 延伸阅读
---
1. Dmitry Soshnikov. Feb 2019. “Writing a Memory Allocator” http://dmitrysoshnikov.com/compilers/writing-a-memory-allocator/ [](https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-memory-allocator/#fnref:1)
2. Sanjay Ghemawat, Paul Menage. “TCMalloc : Thread-Caching Malloc” https://gperftools.github.io/gperftools/tcmalloc.html [](https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-memory-allocator/#fnref:2)
3. runtime: address space conflict at startup using buildmode=c-shared #16936 https://github.com/golang/go/issues/16936 [](https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-memory-allocator/#fnref:3)
4. runtime: use c-shared library in go crashes the program #18976 https://github.com/golang/go/issues/18976 [](https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-memory-allocator/#fnref:4)
5. runtime: 512GB memory limitation https://github.com/golang/go/issues/10460 [](https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-memory-allocator/#fnref:5)
6. “Runtime · Go 1.11 Release Notes” https://golang.org/doc/go1.11#runtime [](https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-memory-allocator/#fnref:6)
7. runtime: use sparse mappings for the heap https://github.com/golang/go/commit/2b415549b813ba36caafa34fc34d72e47ee8335c [](https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-memory-allocator/#fnref:7)
8. OS memory management abstraction layer https://github.com/golang/go/blob/6dd11bcb35cba37f5994c1b9aaaf7d2dc13fd7cf/src/runtime/malloc.go#L346 [](https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-memory-allocator/#fnref:8)
复制代码
本文转载自 Draveness 网站。
原文链接:https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-memory-allocator/
评论