Golang的 内存分配

943 阅读4分钟

以下都是总结其他大佬文章,引用

draveness.me/golang/docs…

studygolang.com/articles/15…

juejin.cn/post/684490…

内存空间包括两个重要的区域,栈区和堆区。 栈区:函数调用的参数、返回值、局部变量都会存储在这里。

堆:堆中的对象都会由内存分配器分配并由垃圾回收器回收。

当用户程序申请内存时,会通过内存分配器申请新的内存,而内存分配器将从堆中初始化对应的内存区域

分配器类型:

线性分配器:此时内存空间类似于一个数组,每次分配候指定大小内存后修改指针位置。

如果有一部分被释放,那么是无法重用这块内存的。

空闲链表分配器:

Go的堆内存布局

go在程序启动时,会分配一块连续的虚拟内存

理解Go的内存分配分两个部分讲解

1).堆内存的结构

2).内存管理组件

第一部分先理解堆内存的构成,第二部分理解go是如何管理这些堆内存的

堆内存的结构

arena

堆上真正存放对象的地方,按页来存储对象,每页为8KB

bitmap

有两个功能:

1.用来标识arena中哪些地址保存了对象(占一位)。

2.标记此对象是否被gc标记过

bitmap 从高地址向低地址增加 而arean从低向高,他们向两边扩散

spans

这一部分中存储了mspan的指针

mspan:内存管理单元,每个内存管理单元会管理几页的内存,一页的大小是8KB

Q:

为什么需要mspan?

A:

(1).如果创立了很多对象,我们遍历地址空间,最差的情况要从头遍历到尾才能找到合适的区域将其放入。因此我们将一个地址空间分为多个地址空间去管理,每个地址空间都管理不同大小的对象。这时当需要有新空间存放对象时,在最适合它大小的地址空间中找到对应的位置后将其放入。

(2).将一个地址空间分为多个不同大小的地址空间会降低空间的碎片化。即使有碎片,也会很小。

而mspan就用来管理arena中不同的地址空间

每个mspan 负责管理不同大小对象的空间。因此go的内存模块一共包含了67个跨度类,每个跨度类都存储特定大小的对象和特定数量的页数、对象。

内存管理组件

mspan

mspan是go内存管理的基本单元

mspan的核心字段有

①.其管理的arena片段的起始地址

②.span类型(属于哪种跨度类)

③.next和pre的双向指针

mcache

也称线程缓存

在Go的并发MGP中每次会提前给P申请一部分内存。避免了多个G内存的申请的并发安全问题,那么mcache就是P提前申请的内存。

mcache中重要的结构

1).67*2个mspan

一个存放包含指针的对象,一个存放不包含指针的对象,这样是为了方便垃圾回收,因为不包含指针的对象无需进一步扫描是否引用其他指针对象。

2).微分配器 为16字节以下的对象分配内存空间

这里引入go的对象类型,go将对象分为3类

①.微对象 size < 16B (较小的字符串)

②.小对象 16b <= size <= 32KB

③.大对象 size > 32KB

微对象和小对象都会使用线程缓存分配对象

微对象有一个专门的微型分配器,也就是在mcache中

小对象会去mcache中查找大小合适的span,当mcache中没有空间给他分配,将会去mcentral获取新的空闲内存

mcentral

也称中心缓存 为所有mcache提供切分好的mspan,当mcache中的mspan使用完后,需要向mcache申请mspan。

mcentral的结构

每个mcentral都保存了一个特定类型的mspan(67中的一种),但是这些mspan都是全局的,因此每当申请mspan时需要进行并发安全处理。

每个mcentral不仅包含了其类型还包含了两个mspan的列表。

nonempty : 指还有内存可用的list

empty : 指无内存可用的list

从mcentral获取的步骤如下:

1.加锁

2.从nonempty获取一个可用span,并将其从nonempty中删除

3.将取出的span放入empty中

4.解锁

5.线程将该mspan存入mcache

假如mcentral中没有了span,那么我们去mheap中申请内存。

mheap

也称页堆 所有对象都由该结构体统一管理,可以当做go持有的整个堆空间

结构如图

mcache是在MGP的P中,所以这里没有他,当申请的对象大于32kb,直接通过堆分配。

贴一张画的非常好的图