go-内存分配器

lmxmimihuhu · · 1764 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

整合两个文章,FixAlloc 和 MSpan


内存模型如下:


FixAlloc

FixAlloc称不上是核心组件,辅助实现整个内存分配器核心的一个基础工具。引入FixAlloc的目的只是用来分配MCacheMSpan两个特定的对象,所以内存分配器中有spanalloccachealloc两个组件。MCache和MSpan两个结构在malloc.h中有定义。

定义在malloc.h文件中的FixAlloc结构如下,比较关键的三个字段是alloc、list和chunk,其他的字段主要都是用来统计一些状态数据的,比如分配了多少内存之类。
struct FixAlloc
{
	uintptr size;
	void *(*alloc)(uintptr);
	void (*first)(void *arg, byte *p);	// called first time p is returned
	void *arg;
	MLink *list;
	byte *chunk;
	uint32 nchunk;
	uintptr inuse;	// in-use bytes now
	uintptr sys;	// bytes obtained from system
};

FixAlloc的内存结构图,一看就很简单,简单到没有出现本文的必要了。

list指针上挂的一个链表,这个链表的每个节点是一个固定大小的内存块,cachealloc中的list存储的内存块大小为sizeof(MCache),而spanalloc中的list存储的内存块大小为sizeof(MSpan)chunk指针始终挂载的是一个128k大的内存块。

FixAlloc提供了三个API,分别是runtime·FixAlloc_Init、runtime·FixAlloc_Alloc和runtime·FixAlloc_Free。

分配一个mcache和mspan的伪代码:

MCache *mcache;
mcache = (MCache *) runtime·FixAlloc_Alloc(cachealloc);

MSpan *mspan;
mspan = (MSpan *) runtime·FixAlloc_Alloc(spanalloc);

这段伪代码展示的是分配一个MCache和MSpan对象,内存分配器并不是直接使用malloc类函数向系统申请,而是走了FixAlloc。使用FixAlloc分配MCache和MSpan对象的时候,首先是查找FixAlloc的list链表,如果list不为空,就直接拿一个内存块返回使用; 如果list为空,就把焦点转移到chunk上去,如果128k的chunk内存中有足够的空间,就切割一块内存出来返回使用,如果chunk内存没有剩余内存的话,就从操作系统再申请128k内存替代老的chunk。FixAlloc的固定对象分配逻辑就这么简单,相反释放逻辑更简单了,释放的对象就是直接放到list中,并不会返回给操作系统。当然mcache的个数基本是稳定的,也就是底层线程个数,但span对象就不一定那么稳定了,所以FixAlloc的内存可能增长的因素就是span的对象太多。


MSpan

MSpan和FixAlloc一样,都是内存分配器的基础工具组件,但和FixAlloc没太大的交集,各自发挥功效而已。span(MSpan简称span)是用来管理一组组page对象,先解释一下page,page就是一个4k大小的内存块而已。span就是将这一个个连续的page给管理起来,注意是连续的page,不是东一个西一个的乱摆设的page。

MSpan结构定义在malloc.h头文件中,代码如下:

struct MSpan
{
	MSpan	*next;		// in a span linked list
	MSpan	*prev;		// in a span linked list
	PageID	start;		// starting page number
	uintptr	npages;		// number of pages in span
	MLink	*freelist;	// list of free objects
	uint32	ref;		// number of allocated objects in this span
	int32	sizeclass;	// size class
	uintptr	elemsize;	// computed from sizeclass or from npages
	uint32	state;		// MSpanInUse etc
	int64   unusedsince;	// First time spotted by GC in MSpanFree state
	uintptr npreleased;	// number of pages released to the OS
	byte	*limit;		// end of data in span
	MTypes	types;		// types of allocated objects in this span
};

span结构比较重要的字段,都出现在上面的结构图中。span的结构中有pre/next两个指针,是用来构造双向链表的。span可能会用在分配小对象(小于等于32k)的过程中,也可能会用于分配大对象(大于32k),在分配不同类型对象的时候,span管理的元数据也大不相同。

npages表示是此span存储的page的个数(比如:上图中就画了3个page),start可以看作一个page指针,指向第一个page,有了第一个page当然就可以算出后面的任何一个page的起始地址了,因为span管理的始终是连续的一组page。这里需要注意start的类型是PageID,由此可以看出这个start保存的并不是第一个page的起始地址,而是第一个page的id值。这个id值是如何算出来的呢?其实给每个page算一个id,是非常简单的事情,只要将这个page的的地址除以4096取整(伪代码:page_addr>>20)即可,当然前提是已经保证好了每个page按4k对齐。这样一来每个page都有一个整数id了,并且任何一个内存地址都可以通过移位算出这个地址属于哪个page。

start是span最重要的一个字段,它维护好了所有的page。sizeclass如果是0的话,就代表这个span是用来分配大对象的,其他值是分配小对象。在分配小对象的时候,start字段维护的所有page,最后将会被切分成一个一个的连续内存块,内存块大小是小对象的大小,这些切分出来的内存块将被链接成为一个链表挂在freelist字段上。分配大对象的时候,freelist就没什么用了。

span干的活,也就这么一点,反正就是管理一组连续的page。内存分配器中的每个page都会属于一个span,page永远不会独立存在。span相关的API有:

// 初始化一个span结构,将分配的page放入到这个span中。
void	runtime·MSpan_Init(MSpan *span, PageID start, uintptr npages);

// 下面这些都是操作span构成的双向链表了。
void	runtime·MSpanList_Init(MSpan *list);
bool	runtime·MSpanList_IsEmpty(MSpan *list);
void	runtime·MSpanList_Insert(MSpan *list, MSpan *span);
void	runtime·MSpanList_Remove(MSpan *span);

Over ...


有疑问加站长微信联系(非本文作者)

本文来自:CSDN博客

感谢作者:lmxmimihuhu

查看原文:go-内存分配器

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1764 次点击  
加入收藏 微博
上一篇:golang
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传