图解Go语言内存分配

STM32F103 实例应用(10)——I2C读写24C32 一、应用简介 本文介绍24C32的读写,基于野火的软件I2C程序。 board_i2c.h /**=========================================================================== @file board_i2c.h @brief 本文件是用于I2C驱动 @author 青梅煮久 @version r0.1 @date 2021/01/13 -------------------------. 阅读详情

Go语言内置运行时(就是runtime),抛弃了传统的内存分配方式,改为自主管理。这样可以自主地实现更好的内存使用模式,比如内存池、预分配等等。这样,不会每次内存分配都需要进行系统调用。

Golang运行时的内存分配算法主要源自 Google 为 C 语言开发的 TCMalloc算法,全称 Thread-CachingMalloc。核心思想就是把内存分为多级管理,从而降低锁的粒度。它将可用的堆内存采用二级分配的方式进行管理:每个线程都会自行维护一个独立的内存池,进行内存分配时优先从该内存池中分配,当内存池不足时才会向全局内存池申请,以避免不同线程对全局内存池的频繁竞争。

为了更好的阅读体验,手动贴上文章目录:

640?wx_fmt=png

基础概念

Go在程序启动的时候,会先向操作系统申请一块内存(注意这时还只是一段虚拟的地址空间,并不会真正地分配内存),切成小块后自己进行管理。

申请到的内存块被分配了三个区域,在X64上分别是512MB,16GB,512GB大小。

640?wx_fmt=png

arena区域就是我们所谓的堆区,Go动态分配的内存都是在这个区域,它把内存分割成 8KB大小的页,一些页组合起来称为 mspan。

bitmap区域标识 arena区域哪些地址保存了对象,并用 4bit标志位表示对象是否包含指针、 GC标记信息。 bitmap中一个 byte大小的内存对应 arena区域中4个指针大小(指针大小为 8B )的内存,所以 bitmap区域的大小是 512GB/(4*8B)=16GB。如下图:

640?wx_fmt=png

从上图其实还可以看到bitmap的高地址部分指向arena区域的低地址部分,也就是说bitmap的地址是由高地址向低地址增长的。

spans区域存放 mspan(也就是一些 arena分割的页组合起来的内存管理基本单元,后文会再讲)的指针,每个指针对应一页,所以 spans区域的大小就是 512GB/8KB*8B=512MB。除以8KB是计算 arena区域的页数,而最后乘以8是计算 spans区域所有指针的大小。创建 mspan的时候,按页填充对应的 spans区域,在回收 object时,根据地址很容易就能找到它所属的 mspan。

内存管理单元

mspan:Go中内存管理的基本单元,是由一片连续的 8KB的页组成的大块内存。注意,这里的页和操作系统本身的页并不是一回事,它一般是操作系统页大小的几倍。一句话概括: mspan是一个包含起始地址、 mspan规格、页的数量等内容的双端链表。

每个 mspan按照它自身的属性 SizeClass的大小分割成若干个 object,每个 object可存储一个对象。并且会使用一个位图来标记其尚未使用的 object。属性 SizeClass决定 object大小,而 mspan只会分配给和 object尺寸大小接近的对象,当然,对象的大小要小于 object大小。还有一个概念: SpanClass,它和 SizeClass的含义差不多,

Size_Class = Span_Class / 2

这是因为其实每个 SizeClass有两个 mspan,也就是有两个 SpanClass。其中一个分配给含有指针的对象,另一个分配给不含有指针的对象。这会给垃圾回收机制带来利好,之后的文章再谈。

如下图, mspan由一组连续的页组成,按照一定大小划分成 object。

640?wx_fmt=png

Go1.9.2里 mspan的 SizeClass共有67种,每种 mspan分割的object大小是8*2n的倍数,这个是写死在代码里的:

// path: /usr/local/go/src/runtime/sizeclasses.go	
const _NumSizeClasses = 67	
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}

根据 mspan的 SizeClass可以得到它划分的 object大小。 比如 SizeClass等于3, object大小就是32B。 32B大小的object可以存储对象大小范围在17B~32B的对象。而对于微小对象(小于16B),分配器会将其进行合并,将几个对象分配到同一个 object中。

数组里最大的数是32768,也就是32KB,超过此大小就是大对象了,它会被特别对待,这个稍后会再介绍。顺便提一句,类型 SizeClass为0表示大对象,它实际上直接由堆内存分配,而小对象都要通过 mspan来分配。

对于mspan来说,它的 SizeClass会决定它所能分到的页数,这也是写死在代码里的:

// path: /usr/local/go/src/runtime/sizeclasses.go	
const _NumSizeClasses = 67	
var class_to_allocnpages = [_NumSizeClasses]uint8{0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1, 3, 2, 3, 1, 3, 2, 3, 4, 5, 6, 1, 7, 6, 5, 4, 3, 5, 7, 2, 9, 7, 5, 8, 3, 10, 7, 4}

比如当我们要申请一个 object大小为 32B的 mspan的时候,在classtosize里对应的索引是3,而索引3在 class_to_allocnpages数组里对应的页数就是1。

mspan结构体定义:

// path: /usr/local/go/src/runtime/mheap.go	
type mspan struct {	
    //链表后向指针,用于将span链接起来	
    next *mspan 	
    //链表前向指针,用于将span链接起来	
    prev *mspan 	
    // 起始地址,也即所管理页的地址	
    startAddr uintptr 	
    // 管理的页数	
    npages uintptr 	
    // 块个数,表示有多少个块可供分配	
    nelems uintptr 	
    //分配位图,每一位代表一个块是否已分配	
    allocBits *gcBits 	
    // 已分配块的个数	
    allocCount uint16 	
    // class表中的class ID,和Size Classs相关	
    spanclass spanClass  	
    // class表中的对象大小,也即块大小	
    elemsize uintptr 	
}

我们将 mspan放到更大的视角来看:

640?wx_fmt=png

上图可以看到有两个 S指向了同一个 mspan,因为这两个 S指向的 P是同属一个 mspan的。所以,通过 arena上的地址可以快速找到指向它的 S,通过 S就能找到 mspan,回忆一下前面我们说的 mspan区域的每个指针对应一页。

假设最左边第一个 mspan的 SizeClass等于10,根据前面的 class_to_size数组,得出这个 msapn分割的 object大小是144B,算出可分配的对象个数是 8KB/144B=56.89个,取整56个,所以会有一些内存浪费掉了,Go的源码里有所有 SizeClass的 mspan浪费的内存的大小;再根据 class_to_allocnpages数组,得到这个 mspan只由1个 page组成;假设这个 mspan是分配给无指针对象的,那么 spanClass等于20。

startAddr直接指向 arena区域的某个位置,表示这个 mspan的起始地址, allocBits指向一个位图,每位代表一个块是否被分配了对象; allocCount则表示总共已分配的对象个数。

这样,左起第一个 mspan的各个字段参数就如下图所示:

640?wx_fmt=png

内存管理组件

内存分配由内存分配器完成。分配器由3种组件构成: mcache, mcentral, mheap。

mcache

mcache:每个工作线程都会绑定一个mcache,本地缓存可用的 mspan资源,这样就可以直接给Goroutine分配,因为不存在多个Goroutine竞争的情况,所以不会消耗锁资源。

mcache的结构体定义:

//path: /usr/local/go/src/runtime/mcache.go	
type mcache struct {	
    alloc [numSpanClasses]*mspan	
}	
numSpanClasses = _NumSizeClasses << 1

mcache用 SpanClasses作为索引管理多个用于分配的 mspan,它包含所有规格的 mspan。它是 _NumSizeClasses的2倍,也就是 67*2=134,为什么有一个两倍的关系,前面我们提到过:为了加速之后内存回收的速度,数组里一半的 mspan中分配的对象不包含指针,另一半则包含指针。

对于无指针对象的 mspan在进行垃圾回收的时候无需进一步扫描它是否引用了其他活跃的对象。 后面的垃圾回收文章会再讲到,这次先到这里。

640?wx_fmt=png

mcache在初始化的时候是没有任何 mspan资源的,在使用过程中会动态地从 mcentral申请,之后会缓存下来。当对象小于等于32KB大小时,使用 mcache的相应规格的 mspan进行分配。

mcentral

mcentral:为所有 mcache提供切分好的 mspan资源。每个 central保存一种特定大小的全局 mspan列表,包括已分配出去的和未分配出去的。 每个 mcentral对应一种 mspan,而 mspan的种类导致它分割的 object大小不同。当工作线程的 mcache中没有合适(也就是特定大小的)的 mspan时就会从 mcentral获取。

mcentral被所有的工作线程共同享有,存在多个Goroutine竞争的情况,因此会消耗锁资源。结构体定义:

//path: /usr/local/go/src/runtime/mcentral.go	
type mcentral struct {	
    // 互斥锁	
    lock mutex 	
    // 规格	
    sizeclass int32 	
    // 尚有空闲object的mspan链表	
    nonempty mSpanList 	
    // 没有空闲object的mspan链表,或者是已被mcache取走的msapn链表	
    empty mSpanList 	
    // 已累计分配的对象个数	
    nmalloc uint64 	
}

用图来表示:

640?wx_fmt=png

empty表示这条链表里的 mspan都被分配了 object,或者是已经被 cache取走了的 mspan,这个 mspan就被那个工作线程独占了。而 nonempty则表示有空闲对象的 mspan列表。每个 central结构体都在 mheap中维护。

简单说下 mcache从 mcentral获取和归还 mspan的流程:

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

  • 归还 加锁;将 mspan从 empty链表删除;将 mspan加入到 nonempty链表;解锁。

mheap

mheap:代表Go程序持有的所有堆空间,Go程序使用一个 mheap的全局对象 _mheap来管理堆内存。

当 mcentral没有空闲的 mspan时,会向 mheap申请。而 mheap没有资源时,会向操作系统申请新内存。 mheap主要用于大对象的内存分配,以及管理未切割的 mspan,用于给 mcentral切割成小对象。

同时我们也看到, mheap中含有所有规格的 mcentral,所以,当一个 mcache从 mcentral申请 mspan时,只需要在独立的 mcentral中使用锁,并不会影响申请其他规格的 mspan。

mheap结构体定义:

//path: /usr/local/go/src/runtime/mheap.go	
type mheap struct {	
    lock mutex	
    // spans: 指向mspans区域,用于映射mspan和page的关系	
    spans []*mspan 	
    // 指向bitmap首地址,bitmap是从高地址向低地址增长的	
    bitmap uintptr 	
    // 指示arena区首地址	
    arena_start uintptr 	
    // 指示arena区已使用地址位置	
    arena_used  uintptr 	
    // 指示arena区末地址	
    arena_end   uintptr 	
    central [67*2]struct {	
        mcentral mcentral	
        pad [sys.CacheLineSize - unsafe.Sizeof(mcentral{})%sys.CacheLineSize]byte	
    }	
}

用图来表示:

640?wx_fmt=png

上图我们看到,bitmap和arena_start指向了同一个地址,这是因为bitmap的地址是从高到低增长的,所以他们指向的内存位置相同。

内存分配流程

上一篇文章《Golang之变量去哪儿》中我们提到了,变量是在栈上分配还是在堆上分配,是由逃逸分析的结果决定的。通常情况下,编译器是倾向于将变量分配到栈上的,因为它的开销小,最极端的就是"zero garbage",所有的变量都会在栈上分配,这样就不会存在内存碎片,垃圾回收之类的东西。

Go的内存分配器在分配对象时,根据对象的大小,分成三类:小对象(小于等于16B)、一般对象(大于16B,小于等于32KB)、大对象(大于32KB)。

大体上的分配流程:

  • >32KB 的对象,直接从mheap上分配;

  • <=16B 的对象使用mcache的tiny分配器分配;

  • (16B,32KB] 的对象,首先计算对象的规格大小,然后使用mcache中相应规格大小的mspan分配;

    • 如果mcache没有相应规格大小的mspan,则向mcentral申请

    • 如果mcentral没有相应规格大小的mspan,则向mheap申请

    • 如果mheap中也没有合适大小的mspan,则向操作系统申请

总结

Go语言的内存分配非常复杂,它的一个原则就是能复用的一定要复用。源码很难追,后面可能会再来一篇关于内存分配的源码阅读相关的文章。简单总结一下本文吧。

文章从一个比较粗的角度来看Go的内存分配,并没有深入细节。一般而言,了解它的原理,到这个程度也可以了。

  • Go在程序启动时,会向操作系统申请一大块内存,之后自行管理。

  • Go内存管理的基本单元是mspan,它由若干个页组成,每种mspan可以分配特定大小的object。

  • mcache, mcentral, mheap是Go内存管理的三大组件,层层递进。mcache管理线程在本地缓存的mspan;mcentral管理全局的mspan供所有线程使用;mheap管理Go的所有动态分配内存。

  • 极小对象会分配在一个object中,以节省资源,使用tiny分配器分配内存;一般小对象通过mspan分配内存;大对象则直接由mheap分配内存。

碳酸钙岩石酸蚀酸溶COMSOL建模:非均质对酸蚀通道的影响 多孔介质中的流动与化学反应耦合是石油工程和地下资源开发中的核心问题,酸液与岩石矿物的溶蚀过程直接关系到酸化增产效果。在碳酸盐岩储层中,盐酸与方解石的反应速率极快,酸液会优先沿高渗透区域推进,形成指状酸蚀通道,即wormhole。传统均质模型难以描述这种局部溶蚀行为,而数值模拟工具COMSOL通过耦合达西定律、稀物质传递与孔隙度演化方程,能够定量刻画酸蚀路径与渗透率的变化规律。工程实践中,模拟结果可用于预测酸液突破体积、优化注入排量,并为基质酸化方案设计提供依据。本文围绕碳酸钙岩石酸蚀酸溶COMSOL模型,探 阅读详情

相关推荐

09、Flutter FFI Dart Native API

本文介绍了 Dart Native API 的基本用法,解决了 Dart 与 C 异步通讯的问题

eieihihi的专栏 4403

Go 内存分配

转载: 图解Golang的内存分配: https://i6448038.github.io/2019/05/18/golang-mem/ 文章目录1 Go的内存分配核心思想2 Go的内存结构2.1 arena2.2 spans2.3 bitmap3 内存管理组件3.1 mspan3.2 mcache3.3 mcentral3.4 mheap4 Go内存分配流程总结4.1 对象分三种:4.2 分配顺序 1 Go的内存分配核心思想 Go是内置运行时的编程语言(runtime),像这种内置运行时的编程语言通常会抛

随心的专栏 1110

你不必懂AI,但必须会用AI:写给所有焦虑中的职场人

AI时代不必焦虑学技术,关键在于用AI解决实际问题。文章指出,99%的人只需成为AI使用者而非建造者,核心竞争力在于结合业务痛点运用AI工具提升效率。与其盲目学习工具,不如梳理工作流程,识别可优化环节,让AI赋能重复性、标准化任务。行业经验与AI工具的结合才是真正的职场护城河。建议从具体工作场景出发,寻找匹配的AI解决方案,迈出应用第一步。

hundred98的博客 720

go 内存分配管理

在cache中,1代表未分配的内存,0代表已分配的内存。searchAddr有一个重要的设定是它前面的地址一定是已经分配过的,因此在查找时,只需要向searchAddr地址的后方查找即可跳过已经查找的节点,减少查找的时间。思考一下,每一种类型的mspan仅用于分配固定大小的内存块,假设Go程序申请1字节的内存,Go语言分配器也会选择第1种类型的mspan,而该类型的mspan负责分配的内存块大小固定为8字节,即有7字节的内存被浪费了,浪费率比例等于7/8=0.785=87.5%。本节主要关注内存的分配。

waeceo的专栏 1534

ModelSim 2020.4破解失败?手把手教你解决‘Unable to checkout viewer license‘报错(附环境变量配置)

本文针对ModelSim 2020.4启动时遇到的‘Unable to checkout viewer license’报错,提供了从理解许可证机制到分步修复的完整指南。文章深入解析了viewer license与图形用户界面(GUI)的关系,并详细说明了通过生成有效许可证文件、精准配置系统环境变量及替换关键文件来解决图形界面启动失败的问题,帮助用户彻底修复并理解其背后的逻辑。

k6l7m8n9的博客 993

【ARM Coresight | AMBA BUS | Cache 与 MMU/MPU | CoreLink | GCC | CSH 专栏导读】

1. ARM Coresight(SoC-400/SoC-600) 专栏 2. ARMv8/v9 CPU/Cache 专栏 3. Linux ARM AMBA 总线 专栏 4. Trace32/DS-5 专栏。。。

CodingCos的博客 8245

分配单元大小是什么意思(分配单元大小)

4、反之,如果用较小的簇来保存较大的文件,那么这个文件将被写入更多的簇,这就意味着增大了文件被写入不相邻的扇区以至不相邻的磁道(就是所谓文件碎片)的概率,增加了寻道时间,使磁盘性能下降。3、  由于一个簇里面的数据必定是连续的,而较大的簇(例如64KB)所跨越的扇区也必定是连续的扇区,所以选择较大的簇时,一次(不必反复寻道)就可以连续读取较多的数据,提高了磁盘性能。8、但要注意,若选择了大于4K的簇后,该分区的压缩功能将失效,这是因为NTFS不支持压缩大于4096字节的簇。

weixin_48345177的博客 1万+

全面认识PFC和LLC

想象一下,你在拉一辆车,如果你的力气都用在推动车子前进的方向上,这样拉车会比较轻松。而如果你的力气用在不正确的方向,比如侧向、斜向,那就需要更大的力气才能推动车子。PFC就是在帮助电流“朝着正确的方向”流动,这样可以用更少的能量做更多的事情,减少无用的功率损耗。PFC负责将电流方向对齐,让电源效率更高,而LLC负责电压的高效转换,两者配合可以在更宽的负载范围内保持高效率。综上所述,PFC LLC电路是一种高效的电源转换电路,通过PFC模块和LLC模块的组合,实现了从交流电到直流电的高效转换。

weixin_43199439的博客 6105
上一篇: 阿里巴巴 Nacos 分布式配置中心原理
下一篇: 布隆过滤器实战【防止缓存击穿】
码洞
博客等级 码龄10年 41粉丝 137原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值