Go内存分配那些事,就这么简单!

原文连接:mp.weixin.qq.com/s/3gGbJaeuv…html

新老朋友很久不见,我是大彬,这篇文章准备了好久,不是在拖延,而是中间作了一些其余事情,耽搁了一些。git

这篇文章主要介绍Go内存分配和Go内存管理,会轻微涉及内存申请和释放,以及Go垃圾回收。github

从很是宏观的角度看,Go的内存管理就是下图这个样子,咱们今天主要关注其中标红的部分。web

Go内存管理

友情提醒:数据库

文章有点长,建议先收藏,后阅读,绝对是学习内存管理的好资料。编程

本文基于go1.11.2,不一样版本Go的内存管理可能存在差异,好比1.9与1.11的mheap定义就是差异比较大的,后续看源码的时候,请注意你的go版本,但不管你用哪一个go版本,这都是一个优秀的资料,由于内存管理的思想和框架始终未变。数组

Go这门语言抛弃了C/C++中的开发者管理内存的方式:主动申请与主动释放,增长了逃逸分析和GC,将开发者从内存管理中释放出来,让开发者有更多的精力去关注软件设计,而不是底层的内存问题。这是Go语言成为高生产力语言的缘由之一。缓存

咱们不须要精通内存的管理,由于它确实很复杂,但掌握内存的管理,可让你写出更高质量的代码,另外,还能助你定位Bug。性能优化

这篇文章采用层层递进的方式,依次会介绍关于存储的基本知识,Go内存管理的“前辈”TCMalloc,而后是Go的内存管理和分配,最后是总结。这么作的目的是,但愿各位能经过全局的认识和思考,拥有更好的编码思惟和架构思惟。数据结构

最后,这不是一篇源码分析文章,由于Go源码分析的文章已经有不少了,这些源码文章可以帮助你去学习具体的工程实践和奇淫巧计了,文章的末尾会推荐一些优秀文章,若是你对内存感兴趣,建议每一篇都去看一下,挑出本身喜欢的,多花时间研究下。

1. 存储基础知识回顾

这部分咱们简单回顾一下计算机存储体系、虚拟内存、栈和堆,以及堆内存的管理,这部份内容对理解和掌握Go内存管理比较重要,建议忘记或不熟悉的朋友不要跳过。

存储金字塔

img

这幅图表达了计算机的存储体系,从上至下依次是:

  • CPU寄存器
  • Cache
  • 内存
  • 硬盘等辅助存储设备
  • 鼠标等外接设备

从上至下,访问速度愈来愈慢,访问时间愈来愈长。

你有没有思考过下面2个简单的问题,若是没有不妨想一想:

  1. 若是CPU直接访问硬盘,CPU能充分利用吗?
  2. 若是CPU直接访问内存,CPU能充分利用吗?

CPU速度很快,但硬盘等持久存储很慢,若是CPU直接访问磁盘,磁盘能够拉低CPU的速度,机器总体性能就会低下,为了弥补这2个硬件之间的速率差别,因此在CPU和磁盘之间增长了比磁盘快不少的内存。

CPU和内存速率差别

然而,CPU跟内存的速率也不是相同的,从上图能够看到,CPU的速率提升的很快(摩尔定律),然而内存速率增加的很慢,虽然CPU的速率如今增长的很慢了,可是内存的速率也没增长多少,速率差距很大,从1980年开始CPU和内存速率差距在不断拉大,为了弥补这2个硬件之间的速率差别,因此在CPU跟内存之间增长了比内存更快的Cache,Cache是内存数据的缓存,能够下降CPU访问内存的时间。

不要觉得有了Cache就万事大吉了,CPU的速率还在不断增大,Cache也在不断改变,从最初的1级,到后来的2级,到当代的3级Cache,(有兴趣看cache历史)

MBP的CPU和Cache信息

三级Cache分别是L一、L二、L3,它们的速率是三个不一样的层级,L1速率最快,与CPU速率最接近,是RAM速率的100倍,L2速率就降到了RAM的25倍,L3的速率更靠近RAM的速率。

看到这了,你有没有Get到整个存储体系的分层设计自顶向下,速率愈来愈低,访问时间愈来愈长,从磁盘到CPU寄存器,上一层均可以看作是下一层的缓存。

看了分层设计,咱们看一下内存,毕竟咱们是介绍内存管理的文章。

虚拟内存

虚拟内存是当代操做系统必备的一项重要功能了,它向进程屏蔽了底层了RAM和磁盘,并向进程提供了远超物理内存大小的内存空间。咱们看一下虚拟内存的分层设计

虚拟内存原理

上图展现了某进程访问数据,当Cache没有命中的时候,访问虚拟内存获取数据的过程。

访问内存,实际访问的是虚拟内存,虚拟内存经过页表查看,当前要访问的虚拟内存地址,是否已经加载到了物理内存,若是已经在物理内存,则取物理内存数据,若是没有对应的物理内存,则从磁盘加载数据到物理内存,并把物理内存地址和虚拟内存地址更新到页表。

有没有Get到:物理内存就是磁盘存储缓存层

另外,在没有虚拟内存的时代,物理内存对全部进程是共享的,多进程同时访问同一个物理内存存在并发访问问题。引入虚拟内存后,每一个进程都要各自的虚拟内存,内存的并发访问问题的粒度从多进程级别,能够下降到多线程级别

栈和堆

咱们如今从虚拟内存,再进一层,看虚拟内存中的栈和堆,也就是进程对内存的管理。

虚拟内存布局

上图展现了一个进程的虚拟内存划分,代码中使用的内存地址都是虚拟内存地址,而不是实际的物理内存地址。栈和堆只是虚拟内存上2块不一样功能的内存区域:

  • 栈在高地址,从高地址向低地址增加。

  • 堆在低地址,从低地址向高地址增加。

栈和堆相比有这么几个好处

  1. 栈的内存管理简单,分配比堆上快。
  2. 栈的内存不须要回收,而堆须要,不管是主动free,仍是被动的垃圾回收,这都须要花费额外的CPU。
  3. 栈上的内存有更好的局部性,堆上内存访问就不那么友好了,CPU访问的2块数据可能在不一样的页上,CPU访问数据的时间可能就上去了。

堆内存管理

内存管理

咱们再进一层,当咱们说内存管理的时候,主要是指堆内存的管理,由于栈的内存管理不须要程序去操心。这小节看下堆内存管理干的是啥,如上图所示主要是3部分:分配内存块,回收内存块和组织内存块

在一个最简单的内存管理中,堆内存最初会是一个完整的大块,即未分配内存,当来申请的时候,就会从未分配内存,分割出一个小内存块(block),而后用链表把全部内存块链接起来。须要一些信息描述每一个内存块的基本信息,好比大小(size)、是否使用中(used)和下一个内存块的地址(next),内存块实际数据存储在data中。

内存块链表

一个内存块包含了3类信息,以下图所示,元数据、用户数据和对齐字段,内存对齐是为了提升访问效率。下图申请5Byte内存的时候,就须要进行内存对齐。

内存块和对齐

释放内存实质是把使用的内存块从链表中取出来,而后标记为未使用,当分配内存块的时候,能够从未使用内存块中有先查找大小相近的内存块,若是找不到,再从未分配的内存中分配内存。

上面这个简单的设计中还没考虑内存碎片的问题,由于随着内存不断的申请和释放,内存上会存在大量的碎片,下降内存的使用率。为了解决内存碎片,能够将2个连续的未使用的内存块合并,减小碎片。

以上就是内存管理的基本思路,关于基本的内存管理,想了解更多,能够阅读这篇文章《Writing a Memory Allocator》,本节的3张图片也是来自这片文章。

2. TCMalloc

TCMalloc是Thread Cache Malloc的简称,是Go内存管理的起源,Go的内存管理是借鉴了TCMalloc,随着Go的迭代,Go的内存管理与TCMalloc不一致地方在不断扩大,但其主要思想、原理和概念都是和TCMalloc一致的,若是跳过TCMalloc直接去看Go的内存管理,也许你会似懂非懂。

掌握TCMalloc的理念,无需去关注过多的源码细节,就能够为掌握Go的内存管理打好基础,基础打好了,后面知识才扎实。

在Linux里,其实有很多的内存管理库,好比glibc的ptmalloc,FreeBSD的jemalloc,Google的tcmalloc等等,为什么会出现这么多的内存管理库?本质都是在多线程编程下,追求更高内存管理效率:更快的分配是主要目的。

那如何更快的分配内存?

咱们前面提到:

引入虚拟内存后,让内存的并发访问问题的粒度从多进程级别,下降到多线程级别。

这是更快分配内存的第一个层次

同一进程的全部线程共享相同的内存空间,他们申请内存时须要加锁,若是不加锁就存在同一块内存被2个线程同时访问的问题。

TCMalloc的作法是什么呢?为每一个线程预分配一块缓存,线程申请小内存时,能够从缓存分配内存,这样有2个好处:

  1. 为线程预分配缓存须要进行1次系统调用,后续线程申请小内存时,从缓存分配,都是在用户态执行,没有系统调用,缩短了内存整体的分配和释放时间,这是快速分配内存的第二个层次
  2. 多个线程同时申请小内存时,从各自的缓存分配,访问的是不一样的地址空间,无需加锁,把内存并发访问的粒度进一步下降了,这是快速分配内存的第三个层次

基本原理

下面就简单介绍下TCMalloc,细致程度够咱们理解Go的内存管理便可。

声明:我没有研究过TCMalloc,如下介绍根据TCMalloc官方资料和其余博主资料总结而来,错误之处请朋友告知我。

TCMalloc概要图

结合上图,介绍TCMalloc的几个重要概念:

  1. Page:操做系统对内存管理以页为单位,TCMalloc也是这样,只不过TCMalloc里的Page大小与操做系统里的大小并不必定相等,而是倍数关系。《TCMalloc解密》里称x64下Page大小是8KB。
  2. Span:一组连续的Page被称为Span,好比能够有2个页大小的Span,也能够有16页大小的Span,Span比Page高一个层级,是为了方便管理必定大小的内存区域,Span是TCMalloc中内存管理的基本单位。
  3. ThreadCache:每一个线程各自的Cache,一个Cache包含多个空闲内存块链表,每一个链表链接的都是内存块,同一个链表上内存块的大小是相同的,也能够说按内存块大小,给内存块分了个类,这样能够根据申请的内存大小,快速从合适的链表选择空闲内存块。因为每一个线程有本身的ThreadCache,因此ThreadCache访问是无锁的。
  4. CentralCache:是全部线程共享的缓存,也是保存的空闲内存块链表,链表的数量与ThreadCache中链表数量相同,当ThreadCache内存块不足时,能够从CentralCache取,当ThreadCache内存块多时,能够放回CentralCache。因为CentralCache是共享的,因此它的访问是要加锁的。
  5. PageHeap:PageHeap是堆内存的抽象,PageHeap存的也是若干链表,链表保存的是Span,当CentralCache没有内存的时,会从PageHeap取,把1个Span拆成若干内存块,添加到对应大小的链表中,当CentralCache内存多的时候,会放回PageHeap。以下图,分别是1页Page的Span链表,2页Page的Span链表等,最后是large span set,这个是用来保存中大对象的。毫无疑问,PageHeap也是要加锁的。

PageHeap

上文提到了小、中、大对象,Go内存管理中也有相似的概念,咱们瞄一眼TCMalloc的定义:

  1. 小对象大小:0~256KB
  2. 中对象大小:257~1MB
  3. 大对象大小:>1MB

小对象的分配流程:ThreadCache -> CentralCache -> HeapPage,大部分时候,ThreadCache缓存都是足够的,不须要去访问CentralCache和HeapPage,无锁分配加无系统调用,分配效率是很是高的。

中对象分配流程:直接在PageHeap中选择适当的大小便可,128 Page的Span所保存的最大内存就是1MB。

大对象分配流程:从large span set选择合适数量的页面组成span,用来存储数据。

经过本节的介绍,你应当对TCMalloc主要思想有必定了解了,我建议再回顾一下上面的内容。

本节图片皆来自《TCMalloc解密》,图片版权归原做者全部。

精彩文章推荐

本文对于TCMalloc的介绍并很少,重要的是3个快速分配内存的层次,若是想了解更多,可阅读下面文章。

  1. TCMalloc 必读,经过这篇你能掌握TCMalloc的原理和性能,对掌握Go的内存管理有很是大的帮助,虽然现在Go的内存管理与TCMalloc已经相差很大,可是,这是Go内存管理的起源和“大道”,这篇文章顶看十几篇Go内存管理的文章。
  2. TCMalloc解密 可选异常详细,包含大量精美图片,看完得花小时级别,理解就须要更多时间了,看完这篇不须要看其余TCMalloc的文章了。
  3. TCMalloc介绍 可选,算是TCMalloc的文档的中文版,多数是从英文版翻译过来的,若是你英文很差,看看。

3. Go内存管理

前面铺垫了那么多,终于到了本文核心的地方。前面的铺垫不是不重要,相反它们很重要,Go语言内存管理源自前面的基础知识和内存管理思惟,若是你跳过了前面的内容,建议你回头看一看,它能够帮助你更好的掌握Go内存管理。

前文提到Go内存管理源自TCMalloc,但它比TCMalloc还多了2件东西:逃逸分析和垃圾回收,这是2项提升生产力的绝佳武器。

这一大章节,咱们先介绍Go内存管理和Go内存分配,最后涉及一点垃圾回收和内存释放。

Go内存管理的基本概念

前面计算机基础知识回顾,是一种自上而下,从宏观到微观的介绍方式,把目光引入到今天的主题。

Go内存管理的许多概念在TCMalloc中已经有了,含义是相同的,只是名字有一些变化。先给你们上一幅宏观的图,借助图一块儿来介绍。

Go内存管理

Page

与TCMalloc中的Page相同,x64下1个Page的大小是8KB。上图的最下方,1个浅蓝色的长方形表明1个Page。

Span

与TCMalloc中的Span相同,Span是内存管理的基本单位,代码中为mspan一组连续的Page组成1个Span,因此上图一组连续的浅蓝色长方形表明的是一组Page组成的1个Span,另外,1个淡紫色长方形为1个Span。

mcache

mcache与TCMalloc中的ThreadCache相似,mcache保存的是各类大小的Span,并按Span class分类,小对象直接从mcache分配内存,它起到了缓存的做用,而且能够无锁访问

但mcache与ThreadCache也有不一样点,TCMalloc中是每一个线程1个ThreadCache,Go中是每一个P拥有1个mcache,由于在Go程序中,当前最多有GOMAXPROCS个线程在运行,因此最多须要GOMAXPROCS个mcache就能够保证各线程对mcache的无锁访问,线程的运行又是与P绑定的,把mcache交给P刚恰好。

mcentral

mcentral与TCMalloc中的CentralCache相似,是全部线程共享的缓存,须要加锁访问,它按Span class对Span分类,串联成链表,当mcache的某个级别Span的内存被分配光时,它会向mcentral申请1个当前级别的Span。

但mcentral与CentralCache也有不一样点,CentralCache是每一个级别的Span有1个链表,mcache是每一个级别的Span有2个链表,这和mcache申请内存有关,稍后咱们再解释。

mheap

mheap与TCMalloc中的PageHeap相似,它是堆内存的抽象,把从OS申请出的内存页组织成Span,并保存起来。当mcentral的Span不够用时会向mheap申请,mheap的Span不够用时会向OS申请,向OS的内存申请是按页来的,而后把申请来的内存页生成Span组织起来,一样也是须要加锁访问的。

但mheap与PageHeap也有不一样点:mheap把Span组织成了树结构,而不是链表,而且仍是2棵树,而后把Span分配到heapArena进行管理,它包含地址映射和span是否包含指针等位图,这样作的主要缘由是为了更高效的利用内存:分配、回收和再利用。

大小转换

除了以上内存块组织概念,还有几个重要的大小概念,必定要拿出来说一下,不要忽视他们的重要性,他们是内存分配、组织和地址转换的基础。

Go内存大小转换

  1. object size:代码里简称size,指申请内存的对象大小。
  2. size class:代码里简称class,它是size的级别,至关于把size归类到必定大小的区间段,好比size[1,8]属于size class 1,size(8,16]属于size class 2。
  3. span class:指span的级别,但span class的大小与span的大小并无正比关系。span class主要用来和size class作对应,1个size class对应2个span class,2个span class的span大小相同,只是功能不一样,1个用来存放包含指针的对象,一个用来存放不包含指针的对象,不包含指针对象的Span就无需GC扫描了。
  4. num of page:代码里简称npage,表明Page的数量,其实就是Span包含的页数,用来分配内存。

在介绍这几个大小之间的换算前,咱们得先看下图这个表,这个表决定了映射关系。

最上面2行是我手动加的,前3列分别是size class,object size和span size,根据这3列作size、size class和num of page之间的转换。

另外,第4列num of objects表明是当前size class级别的Span能够保存多少对象数量,第5列tail waste是span%obj计算的结果,由于span的大小并不必定是对象大小的整数倍。最后一列max waste表明最大浪费的内存百分比,计算方法在printComment函数中,没搞清为什么这样计算。

仔细看一遍这个表,再向下看转换是如何实现的。

Go内存分配表

在Go内存大小转换那幅图中已经标记各大小之间的转换,分别是数组:class_to_sizesize_to_class*class_to_allocnpages,这3个数组内容,就是跟上表的映射关系匹配的。好比class_to_size,从上表看class 1对应的保存对象大小为8,因此class_to_size[1]=8,span大小为8192Byte,即8KB,为1页,因此class_to_allocnpages[1]=1

Size转换

为什么不使用函数计算各类转换,而是写成数组?

有1个很重要的缘由:空间换时间。你若是仔细观察了,上表中的转换,并不能经过简单的公式进行转换,好比size和size class的关系,并非正比的。这些数据是使用较复杂的公式计算出来的,公式在makesizeclass.go中,这其中存在指数运算与for循环,形成每次大小转换的时间复杂度为O(N*2^N)。另外,对一个程序而言,内存的申请和管理操做是不少的,若是不能快速完成,就是很是的低效。把以上大小转换写死到数组里,作到了把大小转换的时间复杂度直接降到O(1)。

Go内存分配

涉及的概念已经讲完了,咱们看下Go内存分配原理。

Go中的内存分类并不像TCMalloc那样分红小、中、大对象,可是它的小对象里又细分了一个Tiny对象,Tiny对象指大小在1Byte到16Byte之间而且不包含指针的对象。小对象和大对象只用大小划定,无其余区分。

Go内存对象分类

小对象是在mcache中分配的,而大对象是直接从mheap分配的,从小对象的内存分配看起。

小对象分配

Go内存管理

大小转换这一小节,咱们介绍了转换表,size class从1到66共66个,代码中_NumSizeClasses=67表明了实际使用的size class数量,即67个,从0到67,size class 0实际并未使用到。

上文提到1个size class对应2个span class:

numSpanClasses = _NumSizeClasses * 2
复制代码

numSpanClasses为span class的数量为134个,因此span class的下标是从0到133,因此上图中mcache标注了的span class是,span class 0span class 133。每1个span class都指向1个span,也就是mcache最多有134个span。

为对象寻找span

寻找span的流程以下:

  1. 计算对象所需内存大小size
  2. 根据size到size class映射,计算出所需的size class
  3. 根据size class和对象是否包含指针计算出span class
  4. 获取该span class指向的span。

以分配一个不包含指针的,大小为24Byte的对象为例。

根据映射表:

// class bytes/obj bytes/span objects tail waste max waste
// 1 8 8192 1024 0 87.50%
// 2 16 8192 512 0 43.75%
// 3 32 8192 256 0 46.88%
// 4 48 8192 170 32 31.52%
复制代码

size class 3,它的对象大小范围是(16,32]Byte,24Byte恰好在此区间,因此此对象的size class为3。

Size class到span class的计算以下:

// noscan为true表明对象不包含指针
func makeSpanClass(sizeclass uint8, noscan bool) spanClass {
	return spanClass(sizeclass<<1) | spanClass(bool2int(noscan))
}
复制代码

因此,对应的span class为:

span class = 3 << 1 | 1 = 7
复制代码

因此该对象须要的是span class 7指向的span。

从span分配对象空间

Span能够按对象大小切成不少份,这些均可以从映射表上计算出来,以size class 3对应的span为例,span大小是8KB,每一个对象实际所占空间为32Byte,这个span就被分红了256块,能够根据span的起始地址计算出每一个对象块的内存地址。

Span内对象

随着内存的分配,span中的对象内存块,有些被占用,有些未被占用,好比上图,总体表明1个span,蓝色块表明已被占用内存,绿色块表明未被占用内存。

当分配内存时,只要快速找到第一个可用的绿色块,并计算出内存地址便可,若是须要还能够对内存块数据清零。

span没有空间怎么分配对象

span内的全部内存块都被占用时,没有剩余空间继续分配对象,mcache会向mcentral申请1个span,mcache拿到span后继续分配对象。

mcentral向mcache提供span

mcentral和mcache同样,都是0~133这134个span class级别,但每一个级别都保存了2个span list,即2个span链表:

  1. nonempty:这个链表里的span,全部span都至少有1个空闲的对象空间。这些span是mcache释放span时加入到该链表的。
  2. empty:这个链表里的span,全部的span都不肯定里面是否有空闲的对象空间。当一个span交给mcache的时候,就会加入到empty链表。

这2个东西名称一直有点绕,建议直接把empty理解为没有对象空间就行了。

mcentral

实际代码中每1个span class对应1个mcentral,图里把全部mcentral抽象成1个总体了。

mcache向mcentral要span时,mcentral会先从nonempty搜索知足条件的span,若是每找到再从emtpy搜索知足条件的span,而后把找到的span交给mcache。

mheap的span管理

mheap里保存了2棵二叉排序树,按span的page数量进行排序:

  1. free:free中保存的span是空闲而且非垃圾回收的span。
  2. scav:scav中保存的是空闲而且已经垃圾回收的span。

若是是垃圾回收致使的span释放,span会被加入到scav,不然加入到free,好比刚从OS申请的的内存也组成的Span。

mheap

mheap中还有arenas,有一组heapArena组成,每个heapArena都包含了连续的pagesPerArena个span,这个主要是为mheap管理span和垃圾回收服务。

mheap自己是一个全局变量,它其中的数据,也都是从OS直接申请来的内存,并不在mheap所管理的那部份内存内。

mcentral向mheap要span

mcentral向mcache提供span时,若是emtpy里也没有符合条件的span,mcentral会向mheap申请span。

mcentral须要向mheap提供须要的内存页数和span class级别,而后它优先从free中搜索可用的span,若是没有找到,会从scav中搜索可用的span,若是尚未找到,它会向OS申请内存,再从新搜索2棵树,必然能找到span。若是找到的span比需求的span大,则把span进行分割成2个span,其中1个恰好是需求大小,把剩下的span再加入到free中去,而后设置需求span的基本信息,而后交给mcentral。

mheap向OS申请内存

当mheap没有足够的内存时,mheap会向OS申请内存,把申请的内存页保存到span,而后把span插入到free树 。

在32位系统上,mheap还会预留一部分空间,当mheap没有空间时,先从预留空间申请,若是预留空间内存也没有了,才向OS申请。

大对象分配

大对象的分配比小对象省事多了,99%的流程与mcentral向mheap申请内存的相同,因此不重复介绍了,不一样的一点在于mheap会记录一点大对象的统计信息,见mheap.alloc_m()

Go垃圾回收和内存释放

若是只申请和分配内存,内存终将枯竭,Go使用垃圾回收收集再也不使用的span,调用mspan.scavenge()把span释放给OS(并不是真释放,只是告诉OS这片内存的信息无用了,若是你须要的话,收回去好了),而后交给mheap,mheap对span进行span的合并,把合并后的span加入scav树中,等待再分配内存时,由mheap进行内存再分配,Go垃圾回收也是一个很强的主题,计划后面单独写一篇文章介绍。

如今咱们关注一下,Go程序是怎么把内存释放给操做系统的?

释放内存的函数是sysUnused,它会被mspan.scavenge()调用:

// MAC下的实现
func sysUnused(v unsafe.Pointer, n uintptr) {
	// MADV_FREE_REUSABLE is like MADV_FREE except it also propagates
	// accounting information about the process to task_info.
	madvise(v, n, _MADV_FREE_REUSABLE)
}
复制代码

注释说_MADV_FREE_REUSABLEMADV_FREE的功能相似,它的功能是给内核提供一个建议:这个内存地址区间的内存已经再也不使用,能够回收。但内核是否回收,以及何时回收,这就是内核的事情了。若是内核真把这片内存回收了,当Go程序再使用这个地址时,内核会从新进行虚拟地址到物理地址的映射。因此在内存充足的状况下,内核也没有必要马上回收内存。

4. Go栈内存

最后提一下栈内存。从一个宏观的角度看,内存管理不该当只有堆,也应当有栈。

每一个goroutine都有本身的栈,栈的初始大小是2KB,100万的goroutine会占用2G,但goroutine的栈会在2KB不够用时自动扩容,当扩容为4KB的时候,百万goroutine会占用4GB。

关于goroutine栈内存管理,有篇很好的文章,饿了么框架技术部的专栏文章:《聊一聊goroutine stack》,把里面的一段内容摘录下,你感觉下:

能够看到在rpc调用(grpc invoke)时,栈会发生扩容(runtime.morestack),也就意味着在读写routine内的任何rpc调用都会致使栈扩容, 占用的内存空间会扩大为原来的两倍,4kB的栈会变为8kB,100w的链接的内存占用会从8G扩大为16G(全双工,不考虑其余开销),这简直是噩梦。

另外,再推荐一篇曹大翻译的一篇汇编入门文章,里面也介绍了扩栈:第一章: Go 汇编入门 ,顺便入门一下汇编。

5. 总结

内存分配原理就再也不回顾了,强调2个重要的思想:

  1. 使用缓存提升效率。在存储的整个体系中处处可见缓存的思想,Go内存分配和管理也使用了缓存,利用缓存一是减小了系统调用的次数,二是下降了锁的粒度,减小加锁的次数,从这2点提升了内存管理效率。
  2. 以空间换时间,提升内存管理效率。空间换时间是一种经常使用的性能优化思想,这种思想其实很是广泛,好比Hash、Map、二叉排序树等数据结构的本质就是空间换时间,在数据库中也很常见,好比数据库索引、索引视图和数据缓存等,再如Redis等缓存数据库也是空间换时间的思想。

6. 参考资料

除了文章中已经推荐的文章,再推荐几篇值得读的文章:

  1. 全成的内存分配文章,有很多帮助:juejin.im/post/5c888a…
  2. 异常详细的源码分析文章,看完这篇我就不想写源码分析的文章了:www.cnblogs.com/zkweb/p/788…
  3. 从硬件讲起的一篇文章,也是有点意思:www.infoq.cn/article/IEh…
  4. 这篇文章的总流程图很棒:media.newbmiao.com/blog/malloc…

7. 彩蛋

在查阅资料时,多篇文章都提到了这本书《The Linux Programming Interface》,关于Thread Cache有兴趣去读一下本书第31章。

相关文章
相关标签/搜索