Go内存分配机制总结

        GO语言内存管理子系统主要由两部分组成:内存分配器和垃圾回收器(gc)。内存分配器主要解决小对象的分配管理和多线程的内存分配问题。什么是小对象呢?小于等于32k的对象就是小对象,其它都是大对象。小对象的内存分配是通过一级一级的缓存来实现的,目的就是为了提升内存分配释放的速度以及避免内存碎片等问题。

        为了方便描述,先给两张图。

图1 内存分配模型


图2 内存布局结构


图1就是go内存分配模型图,从图中,很容易看到有三大模块:Heap, Cental,Cache。central在实际逻辑中是属于heap的子模块。

下面介绍下这三个概念:

1,Cache:线程私有的,每个线程都有一个cache,用来存放小对象。由于每个线程都有cache,所以获取空闲内存是不用加锁的。cache层的主要目的就是提高小内存的频繁分配释放速度。 我们在写程序的时候,其实绝大多数的内存申请都是小于32k的,属于小对象,因此这样的内存分配全部走本地cache,不用向操作系统申请显然是非常高效的。

cache在源码中结构为MCache,定义在malloc.h文件中,从cache中申请内存的函数是:

void *runtime·MCache_Alloc(MCache *c, int32 sizeclass, uintptr size, int32 zeroed)

参数size是要申请的内存大小,这个大小可能比申请内存时指定的大小稍大。


图3 cache数组元素结构

        从图3可以看到cache有一个容量为61的数组list,每个数组单元挂一个链表,链表的每个节点就是一块可用内存,同一个链表中节点的大小是一样的,但是不用数组单元的链表下的节点大小是不一样的,数组的下标(0-60)就是不同的sizeclass。

        MCache_Alloc分配内存的过程是:根据sizeclass参数从list中取出一个内存块链表,如果这个链表不为空,就把这个链表的可用节点返回;如果为空,说明cache中没有满足此类大小的缓存内存,就要从central中去取一部分此类大小的缓存,再把第一个节点返回使用,其他剩余的内存块挂到这个链表上,为下一次分配作准备。 cache的内存分配逻辑很简单,就是cache取不到就从central取。除去内存分配外,cache上还有很多状态计数器,主要用来统计内存分配情况,如:分配的内存量,缓存的内存量等,用于其它相关类查询内存情况以及profile等。

        cache释放主要有两个条件:a,某个内存链表过长(>=256 )时,会截取此链表的一部分节点,返还给central;b,整个cache缓存过大(>=1M),同样将每个链表返还部分节点给central。

2,Central:所有线程共享的组件,不是独占的,因此需要加锁操作。它其实也是一个缓存,cache的一个上游用户,但缓存的不是小对象内存块,而是一组一组的内存page(一个page4K)。从图2可以看出,在heap结构里,使用了一个0到n的数组来存储了一批central,并不是只有一个central对象。从上面结构定义可以知道这个数组长度位61个元素,也就是说heap里其实是维护了61个central,这61个central对应了cache中的list数组,也就是每一个sizeclass就有一个central。所以,在cache中申请内存时,如果在某个sizeclass的内存链表上找不到空闲内存,那么cache就会向对应的sizeclass的central获取一批内存块。注意,这里central数组的定义里面使用填充字节,这是因为多线程会并发访问不同central避免false sharing。

central在源码中定义的结构为MCentral,定义在malloc.h中:

struct MCentral

{

        Lock;

        int32 sizeclass;

        MSpan nonempty;

        MSpan empty;

        int32 nfree;

};

nonempty和empty两个字段很重要,它们都是MSpan类型(下面讲),这两个字段分别挂一个span节点构造的双向链表,只是这个双向链表的头节点不作使用。nonempty意思是非空,表示这个链表上存储的span节点都是非空状态,也就是说这些span节点都有空闲的内存,empty表示span都是空的,没有空闲可用。一开始,empty就是空的,当nonempty上的一个span被用完后,就会将span移到empty中。

        central内存分配过程: central通过int32 runtime·MCentral_AllocList(MCentral *c, int32 n, MLink **pfirst)方法获取一批内存给cache。这个逻辑也很简单,但是要关注下填充nonempty的情况,也就是central没有空闲内存要向heap申请的情况。这里是调用MCentral_Grow函数实现。首先通过MHeap_Alloc向heap申请一个span,然后将span给切分成central对应的sizeclass的小内存块,将这些小内存块串成链表挂在span的freelist上,最后将span放到nonempty链表中。central一次只申请一个span,这个span含有多少page是根据central对应的sizeclass确定。

        central的释放过程:ceche释放的时候,是将一批小内存块返还给central,central在接收到内存块的时候,会把每个内存块分配返还给对应的span,在把内存返还给span后,如果span先前被用完内存,待在empty中那么此刻就需要把它移到nonempty中,表示又有内存用了。在归还内存块给span后,如果span的每个page内存都回收回来了,也就是没有任何内存被使用了,此刻就将这个span整体归还给heap了。

        central这一层管理的粒度就是span,所以span是一个非常重要的工具组件。

3,Heap:所有底层线程共享,是离go程序最远的一层,也是离OS最近的一层。主要是从OS申请内存交给central。在64位平台,heap从OS申请到的内存地址保留区是136G,而bitmap要8G空间,因此真正可申请的内存就是128G。

        无论是分配小内存还是大对象,都需要通过

MSpan* runtime·MHeap_Alloc(MHeap *h, uintptr npage, int32 sizeclass, int32 acct, int32 zeroed)函数。向heap要内存,不是以字节为单位,而是page(npage参数)。sizeClass为0表示绕过cache和central直接分配大内存。从heap申请的page肯定是连续的,并通过span来管理。所有返回值是一个span而不是page数组。

        heap内存分配过程:分配逻辑位于mheap.c中的MHeap_AllocLocked函数。图2中可以看出,heap结构中包括free和large两个域,free是一个256个单元的数组,每个单元上存储一个span链表,但不同的单元span含有page的个数也是不同的。span含page的个数等于span在数组中的单元下标(index,free【2】含有2个page)。如果一个span的page数超过255,那这个span就被放到large链表中。从heap要内存,要根据page的数目从free或large链表中获取合适的span。如果page大到连large链表中都没有合适的span,就只能调用MHeap_Grow函数从OS中去申请,填充heap,再试图分配。拿到的span所含的page数目大于了请求的page数目,并不会将这个span返回,而是将其拆分成两个span,将剩余span重新放回free或large链表中去。全部返回就太浪费了。

heap的释放:central交还heap内存的时候,主要也就是将span放到free或large链表中去。

heap复杂的不是分配内存,而是要维护很多元数据,比如图2中的map域,维护的就是page到span的映射,也就是任何一块内存在算出page后也就知道它属于哪个span了,这样才能做到正确回收内存。bitmap结构,用来标记内存,为gc服务。

总的来说,小对象的分配顺序是:

canche(返回一个内存块)->central(返回一个包含一系列内存块的链表)->heap(返回一个san)->MM(返回span)

MSpan:

        MSPan是内存分配器的基础工具组件,简称span,是用来管理一组组page对象。page就是一个4k大小的内存块。span将这一个个连续的page给管理起来。


图4 MSpan结构图

MSPan结构体定义在malloc.h头文件中,

struct MSpan{

    MSpan*next;// in a span linked list

    MSpan*prev;// in a span linked list

    PageID start;// starting page number

    uintpt 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是一个双向链表(pre/next)。span可能用于分配小对象,也可以用来分配大对象。分配不同的对象,span管理的元数据不相同。npages表示此span存储的page的个数。start可以看作一个page指针,指向第一个page,这样可以可以很容易的获取后续其它page了,因为它们是连续的。start的类型的pageID,所以可以看出start保存的不是第一个page的起始地址,而是第一个page的id值。这个id是算出来的(page的地址除以4096,当然需要保证每个page按4k对齐)。start是span最重要的字段,小对象时,它维护好所有的page,最后page会被切成一个一个连续的内存,内存块大小是小对象的大小,切分出来的内存块将被链接成一个链表挂在freelist字段上。sizeclass为0时,表示大对象,这个时候freelist就没什么用了。

FixAlloc:

        FixAlloc是辅助实现整个内存分配器核心的一个基础工具。是用来分配MCache和MSPan两个特定对象的。


图5 FixMalloc结构图

Fixalloc结构中关键的三个字段是alloc,list和chunk,其它字段都是用来统计一些状态数据的,比如分配了多少内存之类。list指针上挂了一个链表,链表每个节点都是一个固定大小的内存块,cachealloc中的list存储的内存块大小为sizeof(MCache),而spanalloc中的list存储的内存块大小为sizeof(MSpan)。chunk指针始终挂载的是一个128k大的内存块。

分配span和cache方法如下:

MCache *mcache;

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

MSpan *mspan;

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

FixAlloc分配过程:先查找list链表,如果list不为空,就直接拿一个内存块返回是使用,如果为空,就把焦点一道chunk上,如果128k的chunk内存中有足够的空间,就切割一块内存出来返回使用。如果chunk内存没有剩余的话,就从OS再申请128k内存替代老的chunk。

FixAlloc释放过程:释放的对象直接放到list中,并不会返回给OS。fixalloc内存可能增长的因素就是span对象太对,mcache基本是稳定的,跟底层线程个数一致。

应用层面用new和用make分配内存

new:内建函数new(T)分配了零值填充的T类型的内存空间,并返回其地址。一个*T类型的值。用go的术语说,它返回了一个指针,指向新分配的类型T的零值。使用者用new创建的一个数据结构的实体并可以直接工作。如bytes.Buffer的文档所述:Buffer的零值是一个准备好了的空缓存。

make:内建函数make(T,args)只能创建slice,map和channel,并返回一个有初始值(非零)的T类型,而不是*T。导致这三个类型有所不同的原因是指向数据结构的引用在使用前必须初始化。如,一个slice,是一个包含指向数据(内部array)的指针,长度和容量的三项描述,在这些项目初始化之前,slice为nil。对于slice,map和channel,make初始化了内部的数据结构,填充适当的值。

务必记得 make 仅适用于 map,slice 和 channel,并且返回的不是指针。应当用 new 获得特定的指针

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,293评论 18 399
  • TCMalloc是 Google 开发的内存分配器,在不少项目中都有使用,例如在 Golang 中就使用了类似的算...
    ywhu阅读 1,946评论 0 1
  • Linux 内存管理 1 页的概念 linux 内核中把物理页作为内存分配的最小单位,32位CPU 页的大小通常为...
    赤兔欢阅读 3,221评论 0 5
  • 概述 我们都知道一个进程是与其他进程共享CPU和内存资源的。正因如此,操作系统需要有一套完善的内存管理机制才能防止...
    SylvanasSun阅读 3,749评论 0 25
  • 就因为座位,张扬和钟绍辰成了金萱和许正阳的抨击对象,一到宿舍,金萱是开怀畅骂,总是难逃老太太背对着墙壁喝粥这一事例...
    卓暮言阅读 244评论 0 1