Golang内存管理机制

golang是一门自带垃圾回收的语言,它的内存分配器和tmalloc(thread-caching malloc)很像,大多数情况下是不需要用户自己管理内存的。最近了解了一下golang内存管理,写出来分享一下,不正确的地方请大佬们指出。

那么,如果自己想要实现一门语言的内存管理,应该怎么设计呢?

1.内存池:

应该有一个主要管理内存分配的部分,向系统申请大块内存,然后进行管理和分配。

2.垃圾回收:

当分配的内存使用完之后,不直接归还给系统,而是归还给内存池,方便进行下一次复用。至于垃圾回收选择标记回收,还是分代回收算法应该符合语言设计初衷吧。

3.大小切分:

使用单独的数组或者链表,把需要申请的内存大小向上取整,直接从这个数组或链表拿出对应的大小内存块,方便分配内存。大的对象以页申请内存,小的对象以块来申请,避免内存碎片,提高内存使用率。

4.多线程管理:

每个线程应该有自己的内存块,这样避免同时访问共享区的时候加锁,提升语言的并发性,线程之间通信使用消息队列的形式,一定不要使用共享内存的方式。提供全局性的分配链,如果线程内存不够用了,可向分配链申请内存。

这样的内存分配设计涵盖了大部分语言的,上面的想法其实是把golang语言内存分配抽象出来。其实Java语言也可以以同样的方式理解。内存池就是JVM堆,主要负责申请大块内存;多线程管理方面是使用栈内存,每个线程有自己独立的栈内存进行管理。


golang内存分配器

golang内存分配器主要包含三个数据结构:MHeap,MCentral以及MCache

1.MHeap:分配堆,主要是负责向系统申请大块的内存,为下层MCentral和MCache提供内存服务。他管理的基本单位是MSpan(若干连续内存页的数据结构)

type MSpan struct

{

MSpan *next;

MSpan *prev;

PageId start; // 开始的页号

uintptr npages; // 页数

…..

};

可以看出MSpan是一个双端链表的形式,里面存储了它的一些位置信息。

通过一个基地址+(页号*页大小),就可以定位到这个MSpan的实际内存空间。

type MHeap struct

{

lock mutex;

free [_MaxMHeapList] mSpanList // free lists of given length

freelarge mSpanList // free lists length >= _MaxMHeapList

busy [_MaxMHeapList] mSpanList // busy lists of large objects of given length

busylarge mSpanList

};

free数组以span为序号管理多个链表。当central需要时,只需从free找到页数合适的链表。large链表用于保存所有超出free和busy页数限制的MSpan。

MHeap示意图:

Golang内存管理机制_第1张图片

2.MCache:运行时分配池,不针对全局,而是每个线程都有自己的局部内存缓存MCache,他是实现goroutine高并发的重要因素,因为分配小对象可直接从MCache中分配,不用加锁,提升了并发效率。

type MCache struct

{

tiny byte*; // Allocator cache for tiny objects w/o pointers.

tinysize uintptr;

alloc[NumSizeClasses] MSpan*; // spans to allocate from

};

尽可能将微小对象组合到一个tiny块中,提高性能。

alloc[]用于分配对象,如果没有了,则可以向对应的MCentral获取新的Span进行操作。

线程中分配小对象(16~32K)的过程:

对于 size 介于 16 ~ 32K byte 的内存分配先计算应该分配的 sizeclass,然后去 mcache 里面 alloc[sizeclass] 申请,如果 mcache.alloc[sizeclass] 不足以申请,则 mcache 向 mcentral 申请mcentral 给 mcache 分配完之后会判断自己需不需要扩充,如果需要则想 mheap 申请。

每个线程内申请内存是逐级向上的,首先看MCache是否有足够空间,没有就像MCentral申请,再没有就像MHeap,MHeap向系统申请内存空间。


3.MCentral:作为MHeap和MCache的承上启下的连接。承上,从MHeap申请MSpan;启下,将MSpan划分为各种尺寸的对象提供给MCache使用。

type MCentral struct

{

lock mutex;

sizeClass int32;

noempty mSpanList;

empty mSpanList;

int32 nfree;

……

};

type mSpanList struct {

first *mSpan

last *mSpan

};

sizeclass: 也有成员 sizeclass,用于将MSpan进行切分。

lock: 因为会有多个 P 过来竞争。

nonempty: mspan 的双向链表,当前 mcentral 中可用的 mSpan list。

empty: 已经被使用的,可以认为是一种对所有 mSpan 的 track。MCentral存在于MHeap内。

给对象 object 分配内存的主要流程:

1.object size > 32K,则使用 mheap 直接分配。

2.object size < 16 byte,使用 mcache 的小对象分配器 tiny 直接分配。 (其实 tiny 就是一个指针,暂且这么说吧。)

3.object size > 16 byte && size <=32K byte 时,先使用 mcache 中对应的 size class 分配。

4.如果 mcache 对应的 size class 的 span 已经没有可用的块,则向 mcentral 请求。

5.如果 mcentral 也没有可用的块,则向 mheap 申请,并切分。

6.如果 mheap 也没有合适的 span,则想操作系统申请。


tcmalloc内存分配器介绍

tcmalloc(thread-caching mallo)是google推出的一种内存分配器。

具体策略:全局缓存堆和进程的私有缓存。

1.对于一些小容量的内存申请试用进程的私有缓存,私有缓存不足的时候可以再从全局缓存申请一部分作为私有缓存。

2.对于大容量的内存申请则需要从全局缓存中进行申请。而大小容量的边界就是32k。缓存的组织方式是一个单链表数组,数组的每个元素是一个单链表,链表中的每个元素具有相同的大小。

golang语言中MHeap就是全局缓存堆,MCache作为线程私有缓存。

在文章开头说过,内存池就是利用MHeap实现,大小切分则是在申请内存的时候就做了,同时MCache分配内存时,可以用MCentral去取对应的sizeClass,多线程管理方面则是通过MCache去实现。

总结:

1.MHeap是一个全局变量,负责向系统申请内存,mallocinit()函数进行初始化。如果分配内存对象大于32K直接向MHeap申请。

2.MCache线程级别管理内存池,关联结构体P,主要是负责线程内部内存申请。

3.MCentral连接MHeap与MCache的,MCache内存不够则向MCentral申请,MCentral不够时向MHeap申请内存。

你可能感兴趣的:(golang)