绝地无双
golang是一门自带垃圾回收的语言,它的内存分配器和tmalloc(thread-caching malloc)很像,大多数情况下是不需要用户自己管理内存的。最近了解了一下golang内存管理,写出来分享一下,不正确的地方请大佬们指出。1.内存池:应该有一个主要管理内存分配的部分,向系统申请大块内存,然后进行管理和分配。2.垃圾回收:当分配的内存使用完之后,不直接归还给系统,而是归还给内存池,方便进行下一次复用。至于垃圾回收选择标记回收,还是分代回收算法应该符合语言设计初衷吧。3.大小切分:使用单独的数组或者链表,把需要申请的内存大小向上取整,直接从这个数组或链表拿出对应的大小内存块,方便分配内存。大的对象以页申请内存,小的对象以块来申请,避免内存碎片,提高内存使用率。4.多线程管理:每个线程应该有自己的内存块,这样避免同时访问共享区的时候加锁,提升语言的并发性,线程之间通信使用消息队列的形式,一定不要使用共享内存的方式。提供全局性的分配链,如果线程内存不够用了,可向分配链申请内存。这样的内存分配设计涵盖了大部分语言的,上面的想法其实是把golang语言内存分配抽象出来。其实Java语言也可以以同样的方式理解。内存池就是JVM堆,主要负责申请大块内存;多线程管理方面是使用栈内存,每个线程有自己独立的栈内存进行管理。golang内存分配器golang内存分配器主要包含三个数据结构:MHeap,MCentral以及MCache1.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 lengthfreelarge mSpanList // free lists length >= _MaxMHeapListbusy [_MaxMHeapList] mSpanList // busy lists of large objects of given lengthbusylarge mSpanList};free数组以span为序号管理多个链表。当central需要时,只需从free找到页数合适的链表。large链表用于保存所有超出free和busy页数限制的MSpan。MHeap示意图: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 *mSpanlast *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申请内存。