这篇文章主要介绍Go内存分配和Go内存管理,会轻微涉及内存申请和释放,以及Go垃圾回收。
从非常宏观的角度看,Go的内存管理就是下图这个样子,我们今天主要关注其中标红的部分。
友情提醒:
文章有点长,建议先收藏,后阅读,绝对是学习内存管理的好资料。
本文基于go1.11.2,不同版本Go的内存管理可能存在差别,比如1.9与1.11的mheap定义就是差别比较大的,后续看源码的时候,请注意你的go版本,但无论你用哪个go版本,这都是一个优秀的资料,因为内存管理的思想和框架始终未变。
Go这门语言抛弃了C/C++中的开发者管理内存的方式:主动申请与主动释放,增加了逃逸分析和GC,将开发者从内存管理中释放出来,让开发者有更多的精力去关注软件设计,而不是底层的内存问题。这是Go语言成为高生产力语言的原因之一。
我们不需要精通内存的管理,因为它确实很复杂,但掌握内存的管理,可以让你写出更高质量的代码,另外,还能助你定位Bug。
这篇文章采用层层递进的方式,依次会介绍关于存储的基本知识,Go内存管理的“前辈”TCMalloc,然后是Go的内存管理和分配,最后是总结。这么做的目的是,希望各位能通过全局的认识和思考,拥有更好的编码思维和架构思维。
最后,这不是一篇源码分析文章,因为Go源码分析的文章已经有很多了,这些源码文章能够帮助你去学习具体的工程实践和奇淫巧计了,文章的末尾会推荐一些优秀文章,如果你对内存感兴趣,建议每一篇都去看一下,挑出自己喜欢的,多花时间研究下。
1. 存储基础知识回顾
这部分我们简单回顾一下计算机存储体系、虚拟内存、栈和堆,以及堆内存的管理,这部分内容对理解和掌握Go内存管理比较重要,建议忘记或不熟悉的朋友不要跳过。
存储金字塔
这幅图表达了计算机的存储体系,从上至下依次是:
- CPU寄存器
- Cache
- 内存
- 硬盘等辅助存储设备
- 鼠标等外接设备
从上至下,访问速度越来越慢,访问时间越来越长。
你有没有思考过下面2个简单的问题,如果没有不妨想想:
- 如果CPU直接访问硬盘,CPU能充分利用吗?
- 如果CPU直接访问内存,CPU能充分利用吗?
CPU速度很快,但硬盘等持久存储很慢,如果CPU直接访问磁盘,磁盘可以拉低CPU的速度,机器整体性能就会低下,为了弥补这2个硬件之间的速率差异,所以在CPU和磁盘之间增加了比磁盘快很多的内存。
然而,CPU跟内存的速率也不是相同的,从上图可以看到,CPU的速率提高的很快(摩尔定律),然而内存速率增长的很慢,_虽然CPU的速率现在增加的很慢了,但是内存的速率也没增加多少,速率差距很大_,从1980年开始CPU和内存速率差距在不断拉大,为了弥补这2个硬件之间的速率差异,所以在CPU跟内存之间增加了比内存更快的Cache,Cache是内存数据的缓存,可以降低CPU访问内存的时间。
不要以为有了Cache就万事大吉了,CPU的速率还在不断增大,Cache也在不断改变,从最初的1级,到后来的2级,到当代的3级Cache,_(有兴趣看cache历史)_。
三级Cache分别是L1、L2、L3,它们的速率是三个不同的层级,L1速率最快,与CPU速率最接近,是RAM速率的100倍,L2速率就降到了RAM的25倍,L3的速率更靠近RAM的速率。
看到这了,你有没有Get到整个存储体系的分层设计?自顶向下,速率越来越低,访问时间越来越长,从磁盘到CPU寄存器,上一层都可以看做是下一层的缓存。
看了分层设计,我们看一下内存,毕竟我们是介绍内存管理的文章。
虚拟内存
虚拟内存是当代操作系统必备的一项重要功能了,它向进程屏蔽了底层了RAM和磁盘,并向进程提供了远超物理内存大小的内存空间。我们看一下虚拟内存的分层设计。
上图展示了某进程访问数据,当Cache没有命中的时候,访问虚拟内存获取数据的过程。
访问内存,实际访问的是虚拟内存,虚拟内存通过页表查看,当前要访问的虚拟内存地址,是否已经加载到了物理内存,如果已经在物理内存,则取物理内存数据,如果没有对应的物理内存,则从磁盘加载数据到物理内存,并把物理内存地址和虚拟内存地址更新到页表。
有没有Get到:物理内存就是磁盘存储缓存层。
另外,在没有虚拟内存的时代,物理内存对所有进程是共享的,多进程同时访问同一个物理内存存在并发访问问题。引入虚拟内存后,每个进程都要各自的虚拟内存,内存的并发访问问题的粒度从多进程级别,可以降低到多线程级别。
栈和堆
我们现在从虚拟内存,再进一层,看虚拟内存中的栈和堆,也就是进程对内存的管理。
上图展示了一个进程的虚拟内存划分,代码中使用的内存地址都是虚拟内存地址,而不是实际的物理内存地址。栈和堆只是虚拟内存上2块不同功能的内存区域:
- 栈在高地址,从高地址向低地址增长。
- 堆在低地址,从低地址向高地址增长。
栈和堆相比有这么几个好处:
- 栈的内存管理简单,分配比堆上快。
- 栈的内存不需要回收,而堆需要,无论是主动free,还是被动的垃圾回收,这都需要花费额外的CPU。
- 栈上的内存有更好的局部性,堆上内存访问就不那么友好了,CPU访问的2块数据可能在不同的页上,CPU访问数据的时间可能就上去了。
堆内存管理
我们再进一层,当我们说内存管理的时候,主要是指堆内存的管理,因为栈的内存管理不需要程序去操心。这小节看下堆内存管理干的是啥,如上图所示主要是3部分:分配内存块,回收内存块和组织内存块。
在一个最简单的内存管理中,堆内存最初会是一个完整的大块,即未分配内存,当来申请的时候,就会从未分配内存,分割出一个小内存块(block),然后用链表把所有内存块连接起来。需要一些信息描述每个内存块的基本信息,比如大小(size)、是否使用中(used)和下一个内存块的地址(next),内存块实际数据存储在data中。
一个内存块包含了3类信息,如下图所示,元数据、用户数据和对齐字段,内存对齐是为了提高访问效率。下图申请5Byte内存的时候,就需要进行内存对齐。
释放内存实质是把使用的内存块从链表中取出来,然后标记为未使用,当分配内存块的时候,可以从未使用内存块中有先查找大小相近的内存块,如果找不到,再从未分配的内存中分配内存。
上面这个简单的设计中还没考虑内存碎片的问题,因为随着内存不断的申请和释放,内存上会存在大量的碎片,降低内存的使用率。为了解决内存碎片,可以将2个连续的未使用的内存块合并,减少碎片。
以上就是内存管理的基本思路,关于基本的内存管理,想了解更多,可以阅读这篇文章《Writing a Memory Allocator》,本节的3张图片也是来自这片文章。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。