不同的编程语言选择不同的方式管理内存,本节会介绍Go语言内存分配器。

Go内存分配的设计思想是:

  • 内存分配算法采用Google的TCMalloc算法,每个线程都会自行维护一个独立的内存池,进行内存分配时优先从该内存池中分配,当内存池不足时才会向加锁向全局内存池申请,减少系统调用并且避免不同线程对全局内存池的锁竞争
  • 把内存切分的非常细小,分为多级管理,以降低锁的粒度
  • 回收对象内存时,并没有将其真正释放掉,只是放回预先分配的大块内存中,以便复用。只有内存闲置过多的时候才会尝试归还给操作系统,降低整体开销。

接下来就开始详细的讲解:

设计原理

  • 用户程序(mutator)
  • 内存分配器(allocator,简称分配器)
  • 垃圾收集器(collector,简称收集器)

当用户程序申请内存时,它会通过内存分配器重新申请新内存。内存分配器会负责从堆中初始化相应的内存区域。

分配方法

内存分配器一般包含两种分配方法:

  • 线性分配器
  • 空闲链表分配器

线性分配器

线性分配器具有较快的执行速度和简单的实现,但是它的缺点就是容易产生内存碎片。

图片来自于面向信仰编程

它很难利用红色的这个内存了。

因此我们需要结合其他的垃圾收集算法来整理存活对象的碎片,定期合并空闲内存。

例如有:

  • 标记压缩法
  • 复制回收
  • 分代回收

空闲的链表分配器

当用户程序申请内存时,空闲链表分配器会依次遍历空闲内存块,找到足够大的内存,然后申请新资源并修改链表。

因为不同的内存块通过指针构成了链表,所以使用这种方式的分配器可以重新利用回收的资源,但是因为分配器分配内存时需要遍历链表,所以它的时间复杂度是O(N)。空闲链表分配器可以选择不同的策略在链表的内存块中进行选择:

  • 首次适应 – 从链表头开始遍历,找到第一个大小大于申请内存的内存块
  • 循环首次适应 – 从上次遍历的结束位置开始遍历,选择第一个大小大于申请内存的内存块
  • 最优适应 – 从链表头遍历整个链表,选择最合适的内存块
  • 隔离适应 – 将内存分割成多个链表,每个链表中的内存块大小相同,申请内存时先找到满足条件的链表,再从链表中选择合适的内存块

Go语言使用的内存分配策略与第4种策略有些相似

该策略会将内存分割成4,8,16,32字节的内存块组成的链表,当我们向内存分配器申请8字节的内存时,它会找到满足条件的内存块并返回。这样可以减少遍历次数。

分级分配

线程缓存分配是用于分配内存的机制,它比glibc种的malloc还要快很多。Go语言的内存分配器借鉴了TCMalloc(线程缓存分配)。其核心理念是使用多级缓存将对象根据大小分类

对象大小

类型 大小
微对象 (0, 16B)
小对象 [16B, 32KB]
大对象 (32KB, 正无穷)

因为程序中的绝大多数对象的大小都在 32KB 以下,而申请的内存大小影响 Go 语言运行时分配内存的过程和开销,所以分别处理大对象和小对象有利于提高内存分配器的性能。

多级缓存

TCMalloc和Go语言运行时分配器都会引入:

  • 线程缓存
  • 中心缓存
  • 页堆

线程缓存属于每一个独立的线程,它能满足线程上绝大多数的分配要求。因为不涉及多线程,所以不需要锁来保护,减少了性能损耗。当线程缓存不能满足要求的时候,运行时会使用中心缓存作为补充解决小对象的内存分配。在遇到32KB以上的对象时,内存分配器会选择页堆直接分配大内存。

虚拟内存布局

Go 语言程序的 1.10 版本在启动时会初始化整片虚拟内存区域,如下所示的三个区域 spansbitmaparena 分别预留了 512MB、16GB 以及 512GB 的内存空间,这些内存并不是真正存在的物理内存,而是虚拟内存:

线性内存

  • spans 区域存储了指向内存管理单元 runtime.mspan 的指针,每个内存单元会管理几页的内存空间,每页大小为 8KB;
  • bitmap 用于标识 arena 区域中的那些地址保存了对象,位图中的每个字节都会表示堆区中的 32 字节是否空闲;
  • arena 区域是真正的堆区,运行时会将 8KB 看做一页,这些内存页中存储了所有在堆上初始化的对象;

缺点:这种机制的实现依赖于一个重要的条件:堆区的内存是连续的

稀疏内存

运行时使用二维heapArena数组管理所有内存,每个单元都会管理64MB的内存空间。

type heapArena struct {bitmap       [heapArenaBitmapBytes]bytespans        [pagesPerArena]*mspanpageInUse    [pagesPerArena / 8]uint8pageMarks    [pagesPerArena / 8]uint8pageSpecials [pagesPerArena / 8]uint8checkmarks   *checkmarksMapzeroedBase   uintptr
}

该结构体中的 bitmapspans 与线性内存中的 bitmapspans 区域一一对应,zeroedBase 字段指向了该结构体管理的内存的基地址。上述设计将原有的连续大内存切分成稀疏的小内存,而用于管理这些内存的元信息也被切成了小块。

地址空间

所有内存最终都要从操作系统中申请,所以Go语言的运行时构建了操作系统的内存管理抽象层

状态 解释
None 内存没有被保留或者映射,是地址空间的默认状态
Reserved 运行时持有该地址空间,但是访问该内存会导致错误
Prepared 内存被保留,一般没有对应的物理内存访问该片内存的行为是未定义的可以快速转换到 Ready 状态
Ready 可以被安全访问

这个是各种状态之间的转换流程:

内存管理组件

Go内存分配器包含几个重要组件:

  • 内存管理单元 --> runtime.mspan
  • 线程缓存 --> runtime.mcache
  • 中心缓存 --> runtime.mcentral
  • 页堆 --> runtime.mheap

所有的 Go 语言程序都会在启动时初始化如上图所示的内存布局,每一个处理器都会分配一个线程缓存 runtime.mcache 用于处理微对象和小对象的分配,它们会持有内存管理单元 runtime.mspan

每个类型的内存管理单元都会管理特定大小的对象,当内存管理单元中不存在空闲对象时,它们会从 runtime.mheap 持有的 134 个中心缓存 runtime.mcentral

这张图很重要,建议先看一下留个印象,然后到时候把文章看完之后可以回来返回观看。

内存管理单元

runtime.mspan是Go语言内存管理的基本单元,该结构体中包含next和prev两个字段。

type mspan struct {next *mspan     // next span in list, or nil if noneprev *mspan     // previous span in list, or nil if nonelist *mSpanList // For debugging. TODO: Remove........
}

//go:notinheap
type mSpanList struct {first *mspan // first span in list, or nil if nonelast  *mspan // last span in list, or nil if none
}

页和内存

每一个runtime.mspan都管理npages个8KB的页,这个页不是操作系统的页,而是其整数倍。

type mspan struct {startAddr uintptr // 起始地址npages    uintptr // 页数freeindex uintptr // 扫描页中空闲对象的初始索引allocBits  *gcBits // 标记内存的占用情况gcmarkBits *gcBits // 标记内存的回收情况allocCache uint64 // allocBits的补码,可用于快速查找内存中未被使用的内存...
}

如上图,当结构体管理的内存不足时,运行时会以页为单位向堆中申请内存。

当用户程序或者线程向 runtime.mspan 申请内存时,它会使用 allocCache 字段以对象为单位在管理的内存中快速查找待分配的空间。如果我们能在内存中找到空闲的内存单元会直接返回,当内存中不包含空闲的内存时,上一级的组件 runtime.mcache 会为调用 runtime.mcache.refill 更新内存管理单元以满足为更多对象分配内存的需求。

状态

type mspan struct {...state       mSpanStateBox...
}

状态一共有4种:

var mSpanStateNames = []string{"mSpanDead","mSpanInUse","mSpanManual","mSpanFree",
}

跨度类

type mspan struct {...spanclass   spanClass...
}

Go有68种不同大小的spanClass,用于小对象的分配

const _NumSizeClasses = 68
var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536,1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}

如果按照序号为1的spanClass(对象规格为8B)分配,每个span占用堆的字节数:8k,mspan可以保存1024个对象

如果按照序号为2的spanClass(对象规格为16B)分配,每个span占用堆的字节数:8k,mspan可以保存512个对象

如果按照序号为67的spanClass(对象规格为32K)分配,每个span占用堆的字节数:32k,mspan可以保存1个对象

大于32k的对象出现时,会直接从heap分配一个特殊的span,这个特殊的span的类型(class)是0, 只包含了一个大对象。

runtime.spanClass 是一个 uint8 类型的整数,它的前 7 位存储着跨度类的 ID,最后一位表示是否包含指针,该类型提供的两个方法能够帮我们快速获取对应的字段。

线程缓存

mcache管理线程在本地缓存的mspan,每个goroutine绑定的P都有一个mcache字段

type mcache struct {alloc [numSpanClasses]*mspan ...
}_NumSizeClasses = 68
numSpanClasses = _NumSizeClasses << 1

mcacheSpan Classes作为索引管理多个用于分配的mspan,它包含所有规格的mspan。它是_NumSizeClasses的2倍,也就是68*2=136,其中*2是将spanClass分成了有指针和没有指针两种,方便与垃圾回收。对于每种规格,有2个mspan,一个mspan不包含指针,另一个mspan则包含指针。对于无指针对象的mspan在进行垃圾回收的时候无需进一步扫描它是否引用了其他活跃的对象。

这也回应了前面spanClass前7位是跨度类的ID,最后一位是是否包含指针。

中心缓存

mcentral管理全局的mspan供所有线程使用,全局mheap变量包含central字段,每个 mcentral 结构都维护在mheap结构内

type mcentral struct {spanclass spanClass // 指当前规格大小partial [2]spanSet // 有空闲object的mspan列表full    [2]spanSet // 没有空闲object的mspan列表
}

每个mcentral管理一种spanClass的mspan,并将有空闲空间和没有空闲空间的mspan分开管理。partial和 full的数据类型为spanSet,表示 mspans集,可以通过pop、push来获得mspans

type spanSet struct {spineLock mutexspine     unsafe.Pointer // 指向[]span的指针spineLen  uintptr        // Spine array length, accessed atomicallyspineCap  uintptr        // Spine array cap, accessed under lockindex headTailIndex  // 前32位是头指针,后32位是尾指针
}

简单说一下mcachemcentral获取和归还mspan的流程:

  • 获取; 加锁,从partial链表找到一个可用的mspan;并将其从partial链表删除;将取出的mspan加入到full链表;将mspan返回给工作线程,解锁。
  • 归还; 加锁,将mspanfull链表删除;将mspan加入到partial链表,解锁。

页堆

runtime.mheap 是内存分配的核心结构体,Go 语言程序会将其作为全局变量存储,而堆上初始化的所有对象都由该结构体统一管理,该结构体中包含两组非常重要的字段,其中一个是全局的中心缓存列表 central,另一个是管理堆区内存区域的 arenas 以及相关字段。

var mheap_ mheap
type mheap struct {lock      mutex    // 全局锁pages     pageAlloc // 页面分配的数据结构allspans []*mspan // 所有通过 mheap_ 申请的mspans// 堆arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena// 所有中心缓存mcentralcentral [numSpanClasses]struct {mcentral mcentralpad      [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte}...
}

初始化

有几个初始化比较重要:

  • 空闲链表分配器
  • 中心缓存

内存管理单元

func (h *mheap) alloc(npages uintptr, spanclass spanClass, needzero bool) *mspan {var s *mspansystemstack(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 分配新的内存管理单元,我们会将该方法的执行过程拆分成两个部分:

如果申请内存大小充足,那么就直接申请成功。如果申请内存大小不充足,那么就会触发扩容。扩容的时候如果申请到内存就说明扩容成功,否则就是扩容失败,运行时会直接终止当前程序。

内存分配

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {mp := acquirem()mp.mallocing = 1c := gomcache()var x unsafe.Pointernoscan := typ == nil || typ.ptrdata == 0if size <= maxSmallSize {if noscan && size < maxTinySize {// 微对象分配} else {// 小对象分配}} else {// 大对象分配}publicationBarrier()mp.mallocing = 0releasem(mp)return x
}

上述代码使用 runtime.gomcache 获取线程缓存并判断申请内存的类型是否为指针。我们从这个代码片段可以看出 runtime.mallocgc 会根据对象的大小执行不同的分配逻辑,在前面的章节也曾经介绍过运行时根据对象大小将它们分成微对象、小对象和大对象,这里会根据大小选择不同的分配逻辑:

  • 微对象 (0, 16B) — 先使用微型分配器,再依次尝试线程缓存、中心缓存和堆分配内存;
  • 小对象 [16B, 32KB] — 依次尝试使用线程缓存、中心缓存和堆分配内存;
  • 大对象 (32KB, +∞) — 直接在堆上分配内存;

下面依次来进行介绍:

微对象

Go 语言运行时将小于 16 字节的对象划分为微对象,它会使用线程缓存上的微分配器提高微对象分配的性能,我们主要使用它来分配较小的字符串以及逃逸的临时变量。微分配器可以将多个较小的内存分配请求合入同一个内存块中,只有当内存块中的所有对象都需要被回收时,整片内存才可能被回收。

微分配器管理的对象不可以是指针类型,管理多个对象的内存块大小 maxTinySize 是可以调整的,在默认情况下,内存块的大小为 16 字节。maxTinySize 的值越大,组合多个对象的可能性就越高,内存浪费也就越严重;maxTinySize 越小,内存浪费就会越少,不过无论如何调整,8 的倍数都是一个很好的选择。

图 7-20 微分配器的工作原理

如上图所示,微分配器已经在 16 字节的内存块中分配了 12 字节的对象,如果下一个待分配的对象小于 4 字节,它会直接使用上述内存块的剩余部分,减少内存碎片,不过该内存块只有所有对象都被标记为垃圾时才会回收。

线程缓存 runtime.mcache 中的 tiny 字段指向了 maxTinySize 大小的块,如果当前块中还包含大小合适的空闲内存,运行时会通过基地址和偏移量获取并返回这块内存:

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {...if size <= maxSmallSize {if noscan && size < maxTinySize {off := c.tinyoffsetif off+size <= maxTinySize && c.tiny != 0 {x = unsafe.Pointer(c.tiny + off)c.tinyoffset = off + sizec.local_tinyallocs++releasem(mp)return x}...}...}...
}

当内存块中不包含空闲的内存时,下面的这段代码会先从线程缓存找到跨度类对应的内存管理单元 runtime.mspan,调用 runtime.nextFreeFast 获取空闲的内存;当不存在空闲内存时,我们会调用 runtime.mcache.nextFree 从中心缓存或者页堆中获取可分配的内存块:

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] = 0if 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 清空空闲内存中的所有数据;

大对象

运行时对于大于 32KB 的大对象会单独处理,我们不会从线程缓存或者中心缓存中获取内存管理单元,而是直接调用 runtime.mcache.allocLarge 分配大片内存:

func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {...if size <= maxSmallSize {...} else {var s *mspanspan = c.allocLarge(size, needzero, noscan)span.freeindex = 1span.allocCount = 1x = unsafe.Pointer(span.base())size = span.elemsize}publicationBarrier()mp.mallocing = 0releasem(mp)return x
}

runtime.mcache.allocLarge 会计算分配该对象所需要的页数,它按照 8KB 的倍数在堆上申请内存:

func (c *mcache) allocLarge(size uintptr, needzero bool, noscan bool) *mspan {npages := size >> _PageShiftif size&_PageMask != 0 {npages++}...s := mheap_.alloc(npages, spc, needzero)mheap_.central[spc].mcentral.fullSwept(mheap_.sweepgen).push(s)s.limit = s.base() + sizeheapBitsForAddr(s.base()).initSpan(s)return s
}

申请内存时会创建一个跨度类为 0 的 runtime.spanClass 并调用 runtime.mheap.alloc 分配一个管理对应内存的管理单元。

总结

  • 首先通过计算使用的大小规格
  • 然后使用mcache中对应大小规格的块分配。
  • 如果mcentral中没有可用的块,则向mheap申请,并根据算法找到最合适的mspan
  • 如果申请到的mspan 超出申请大小,将会根据需求进行切分,以返回用户所需的页数。剩余的页构成一个新的 mspan 放回 mheap 的空闲列表。
  • 如果 mheap 中没有可用 span,则向操作系统申请一系列新的页(最小 1MB)

Go语言设计与实现 -- 内存管理器相关推荐

  1. C++ allocator设计内存管理器

    文章目录 allocator内存管理器 基本属性 类的设计 关键功能的实现 完整的内存管理器 内存管理器的测试:设计自定义的String类. 前情回顾: allocator内存管理类 allocato ...

  2. 大内高手 内存管理器

    分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow 也欢迎大家转载本篇文章.分享知识,造福人民,实现我们中华民族伟大复兴! 大内高手 ...

  3. 大内高手—内存管理器

    大内高手-内存管理器 转载时请注明出处和作者联系方式:http://blog.csdn.net/absurd 作者联系方式:李先静 <xianjimli at hotmail dot com&g ...

  4. 内存管理器(二)边界标识法

    边界标识算法 前言 首先说明,我们这里的内存管理器主要是以模拟各种内存分配算法为主,从内存申请一片内存然后根据我们所选定的数据结构和算法,实现程序的堆空间的分配,至于内存分配详情我们会在Linux内核 ...

  5. 内存编程 c语言 c,C语言编程入门之内存管理

    本篇教程探讨了C语言编程入门之内存管理,希望阅读本篇文章以后大家有所收获,帮助大家对相关内容的理解更加深入. < 自动变量与静态变量 auto自动变量 auto是默认的关键字,如实际中int a ...

  6. 一个简单而强大的单片机内存管理器-不带内存碎片整理

    单片机简单内存管理器 本代码基于无操作系统的STM32单片机开发.功能强大.可申请到地址空间连续的不同大小的内存空间,且用户接口简单,使用方便 转载请注明出处:http://blog.csdn.net ...

  7. 内存管理器剖析:ptmalloc,windows,macOS

    目录 1. Ptmalloc 2. Windows内存管理器 3. Mac OS内存管理器 4.推荐阅读 核心分析器的优势在于它能够将堆内存解析为数百万个单独的内存块,这些内存块由堆内存管理器分配给应 ...

  8. C/C++内存管理器

    C标准库提供了malloc,free,calloc,realloc,C++标准库还提供了new, new[], delete, delete[].这些用来管理内存,看起来够用了,为啥还要自己写一个内存 ...

  9. flink的内存管理器MemoryManager

    Flink中通过MemoryManager来管理内存. 在MemoryManager中,根据要管理的内存的总量和和每个内存页的大小得到内存页的数量生成相应大小数量的内存页来作为可以使用的内存. pub ...

最新文章

  1. 89岁屠呦呦再次震惊世界!视频介绍疟疾、青蒿素和背后的故事
  2. C#委托及事件 详解(讲得比较透彻)
  3. Android中的基础控件CheckBox、RadioButton、ToggleButton、SeekBar
  4. c++数据结构代码整理_抄代码对自己编程提高有用嘛
  5. mos 多路模拟电子开关_【原创】单火线智能开关技术介绍及分析
  6. [bzoj5405]platform
  7. 【Pytorch神经网络理论篇】 03 Pytorch快速上手(三)张量的数据操作
  8. sql server 以10分钟分组 统计人数
  9. http://www.cnoug.org/viewthread.php?tid=9292
  10. c++可视化_数据可视化——如何让你的信息图被记住
  11. java第六次作业 计科1501班 张鹏
  12. Vue+elementUI+springboot+mybatis demo教程(二)
  13. 《玩转D语言系列》一、通过四个版本的 Hello Word 初识D语言
  14. Google 再见 Java
  15. mysql改根用户_MySQL数据库中复位根用户密码的方法 | 很文博客
  16. 计算机启动时硬盘指示灯不亮,电脑启动时没有自检声,硬盘指示灯不亮
  17. 【Windows无法访问指定设备,路径或文件,您可能没有合适的权限访问】
  18. 韩寒首度回应小三事件:望女友妻子和平相处_陕西频道_凤凰网
  19. 【7gyy】支招:自检性能搞定网速慢电脑卡的问题
  20. 艾美捷科研专用西妥昔单抗Cetuximab相关介绍

热门文章

  1. 瓶中阳光——雪莉之美
  2. VC 编译选项的使用点滴 warring lnk4908
  3. 17.机器比人更需要通证
  4. Linux下的常用编程工具初探
  5. 瘦臀瘦腿的最快方法是什么?
  6. 如何选择适合你的兴趣爱好(四十九),现代舞
  7. php类和对象教程,php类和对象
  8. linux 4.6发布时间,Linux Kernel 4.6的第4个维护版本发布
  9. php做网站需要注意什么,浅谈新手做网站应该注意的问题
  10. The Elliptic Curve Digital Signature Algorithm(ECDSA)学习之路 - 签名验签流程