阿里、蚂蚁、晟腾、中科加禾精彩分享 AI 基础设施洞见,现购票可享受 9 折优惠 |AICon 了解详情
写点什么

Go 语言内存分配器的实现原理(下)

  • 2020-03-01
  • 本文字数:10406 字

    阅读完需:约 34 分钟

Go 语言内存分配器的实现原理(下)

微分配器

线程缓存中还包含几个用于分配微对象的字段,下面的这三个字段组成了微对象分配器,专门为 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 方法获取新的内存管理单元,该方法的实现比较复杂,我们可以将其分成以下几个部分:


  1. 从非空链表中查找可以使用的内存管理单元;

  2. 从空闲链表中查找可以使用的内存管理单元;

  3. 调用 runtime.mcentral.grow 从堆中申请新的内存管理单元;

  4. 更新内存管理单元的 allocCache 等字段帮助快速分配内存;


首先我们会在中心缓存的非空链表中查找可用的 runtime.mspan,根据 sweepgen 字段分别进行不同的处理:


  1. 当内存单元等待回收时,将其插入空闲链表队列、调用 runtime.mspan.sweep 清理该单元并返回;

  2. 当内存单元正在被后台回收时,跳过该内存单元;

  3. 当内存单元已经被回收时,将内存单元插入空闲链表队列并返回;


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}
复制代码


无论通过哪种方法获取到了内存单元,该方法的最后都会对内存单元的 allocBitsallocCache 等字段进行更新,让运行时在分配内存时能够快速找到空闲的对象。

扩容

中心缓存的扩容方法 runtime.mcentral.grow 会根据预先计算的 class_to_allocnpagesclass_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 方法,我们能看到该方法初始化了非常多的结构体和字段,不过其中初始化的两类变量比较重要:


  1. spanalloccachealloc 以及 arenaHintAllocruntime.fixalloc 类型的空闲链表分配器;

  2. 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 初始化分配器时,需要传入带初始化的结构体大小等信息,这会帮助分配器分割待分配的内存,该分配器提供了以下两个用于分配和释放内存的方法:


  1. runtime.fixalloc.alloc — 获取下一个空闲的内存空间;

  2. 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 分配新的内存管理单元,我们会将该方法的执行过程拆分成两个部分:


  1. 从堆上分配新的内存页和内存管理单元 runtime.mspan

  2. 初始化内存管理单元并将其加入 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 两种途径从堆中申请内存:


  1. 如果申请的内存比较小,获取申请内存的处理器并尝试调用 runtime.pageCache.alloc 获取内存区域的基地址和大小;

  2. 如果申请的内存比较大或者线程的页缓存中内存不足,会通过 runtime.pageAlloc.alloc 在页堆上申请内存;

  3. 如果发现页堆上的内存不足,会尝试通过 runtime.mheap.grow 进行扩容并重新调用 runtime.pageAlloc.alloc 申请内存;

  4. 如果申请到内存,意味着扩容成功;

  5. 如果没有申请到内存,意味着扩容失败,宿主机可能不存在空闲内存,运行时会直接中止当前程序;


无论通过哪种方式获得内存页,我们都会在该函数中分配新的 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 方法会向操作系统申请更多的内存空间,传入的页数经过对齐可以得到期望的内存大小,我们可以将该方法的执行过程分成以下几个部分:


  1. 通过传入的页数获取期望分配的内存空间大小以及内存的基地址;

  2. 如果 arena 区域没有足够的空间,调用 runtime.mheap.sysAlloc 从操作系统中申请更多的内存;

  3. 扩容 runtime.mheap 持有的 arena 区域并更新页分配器的元信息;

  4. 在某些场景下,调用 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.sysReserveruntime.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}
复制代码


获取新的空闲内存块之后,上述代码会清空空闲内存中的数据、更新构成微对象分配器的几个字段 tinytinyoffset 并返回新的空闲内存。

小对象

小对象是指大小为 16 字节到 32,768 字节的对象以及所有小于 16 字节的指针类型的对象,小对象的分配可以被分成以下的三个步骤:


  1. 确定分配对象的大小以及跨度类 runtime.spanClass

  2. 从线程缓存、中心缓存或者堆中获取内存管理单元并从内存管理单元找到空闲的内存空间;

  3. 调用 runtime.memclrNoHeapPointers 清空空闲内存中的所有数据;


确定待分配的对象大小以及跨度类需要使用预先计算好的 size_to_class8size_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.nextFreeFastruntime.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}
复制代码


找到了空闲的对象后,我们就可以更新内存管理单元的 allocCachefreeindex 等字段并返回该片内存了;如果我们没有找到空闲的内存,运行时会通过 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/


2020-03-01 21:41844

评论

发布
暂无评论
发现更多内容

北京Java培训收费标准是什么

小谷哥

Vue入门指北——css中的js变量

Augus

Vue 9月月更

前端培训学习有哪值得注意的呢?

小谷哥

Vue3入门指北(一)组件API风格

Augus

Vue 9月月更

本周三,全球C++大会8大主题、近40场专题演讲盛大开启,龙蜥2位技术专家参加

OpenAnolis小助手

工具链 ebpf 2022 龙蜥 C++及系统软件技术大会

新来个阿里 P7,仅花 2 小时,撸出一个多线程永动任务,看完直接跪了,真牛逼!

程序知音

大数据培训和大学相关专业哪个更好

小谷哥

都 2022 年了,你真的会用 Python 的 pip 吗?

梦想橡皮擦

9月月更

用区块链思维让可信数据流动起来

旺链科技

区块链 产业区块链 供应链金融 企业号九月金秋榜

前端培训学习哪种方式比较靠谱

小谷哥

Python 中的 super 函数怎么学,怎么解?

梦想橡皮擦

Python 9月月更

阿里P8手写Spring Cloud Alibaba实战学习手册,架构师养成必备

Geek_0c76c3

Java 数据库 开源 程序员 架构

惊艳!阿里内部JDK源码剖析知识手册,由浅入深堪称完美

了不起的程序猿

Java 编程 程序员 jdk源码 JAVA开发

这篇文章教会我用 Python 读取 PDF 文件【收藏即会】

梦想橡皮擦

9月月更

Seal 软件供应链防火墙 v0.2 发布,提供依赖项全局洞察

SEAL安全

DevSecOps 开源安全 软件供应链 软件供应链安全 软件供应链防火墙

BGP劫持原理及如何防御

郑州埃文科技

网络安全 BGP 安全防御

你看过字符画吗?用 Python 自己实现一个吧

梦想橡皮擦

9月月更

【软通动力鸿湖万联扬帆系列“竞”开发板试用体验】折腾”竞“开发板

白晓明

OpenHarmony "竞"开发板体验

java培训软件开发技术收费标准

小谷哥

Ventana Research|是时候把指标中台纳入企业数据架构了!

Kyligence

数据架构 数据管理 指标中台

想要优化K8S集群管理?Cluster API帮你忙 | K8S Internals系列第5期

BoCloud博云

云原生 容器云 K8s 多集群管理

4步成功将三方库——speexdsp移植到OpenHarmony

OpenHarmony开发者

OpenHarmony

中秋发祝福?一套程序让你成为【相亲相爱一家人】群里最靓的仔

梦想橡皮擦

Python 9月月更

Vue入门指北——渲染函数和jsx

Augus

Vue 9月月更

三三复制公排互助系统开发技术详情

开发微hkkf5566

YYEVA动效播放器--动态元素完美呈现新方案

百度Geek说

移动端 企业号九月金秋榜 动画特效 AE插件

因为K8s,阿鹏遇到了人生中第一次职业危机....

嘉为蓝鲸

#运维

Python 几行代码实现一键抠图,收费应用 BYEBYE

梦想橡皮擦

9月月更

5 行 Python 爬虫代码,就能采集每日资讯@_@

梦想橡皮擦

9月月更

什么是架构,什么是架构师?

源字节1号

软件开发 软件架构

跨模态学习能力再升级,EasyNLP电商文图检索效果刷新SOTA

阿里云大数据AI技术

深度学习 PyTorch 图文检索 企业号九月金秋榜

Go 语言内存分配器的实现原理(下)_行业深度_Draveness_InfoQ精选文章