本文由 简悦 SimpRead 转码, 原文地址 juejin.cn
在说明golang内存分配之前,先了解下Linux系统内存相关的基础知识,有助于理解golang内存分配原理。 在早期内存管理中,如果程序太大,超过了空闲内存容量,就没有办法把全部程序装入到内存,这时怎么办? 在许多年前,人们采用了一种叫做覆盖技术,这样一种解决方案。 就是把程…
在说明 golang 内存分配之前,先了解下 Linux 系统内存相关的基础知识,有助于理解 golang 内存分配原理。
在早期内存管理中,如果程序太大,超过了空闲内存容量,就没有办法把全部程序装入到内存,这时怎么办? 在许多年前,人们采用了一种叫做覆盖技术,这样一种解决方案。
这是一种什么样的解决方案? 就是把程序分为若干个部分,称为覆盖块(overlay),核心思想就是分解(跟现代架构技术中分解、分模块思想很相近)。然后只把那些需要用到的指令和数据保存在内存中,而把其余的指令和数据保存在内存外。关键是需要程序员手动来分块。
这种技术有什么问题呢? 这种技术必须由程序员手工把一个大的程序划分为若干个小的功能模块,并确定各个模块之间的调用关系。手工做这种事情很费时费力,使得编程复杂度增加。但是,程序员总是爱 “偷懒” 的,于是,人们去寻找更好的方案。
这个方案就是虚拟内存技术,它的基本思路: 程序运行进程的总大小可以超过实际可用的物理内存的大小。每个进程都可以有自己独立的虚拟地址空间。然后通过 CPU 和 MMU 把虚拟内存地址转换为实际物理地址。
这个就相当于在物理内存和程序之间增加了一个中间层,虚拟内存。 虚拟存储也可以看作是对内存的一种抽象。而且这种抽象带来诸多好处:
- 它将内存看成是一个存储在磁盘上的地址空间的高速缓存,在内存中只保留了活动区域,可以根据需要在磁盘和内存间来回传送数据,高效使用内存。
- 它为每个进程提供了一致的地址空间,简化了存储的管理。
- 对进程起到保护作用,不被其他进程地址空间破坏,因为每个进程的地址空间都是相互独立。
(程序:静态的程序;进程:动态的,可以看作是程序的一个实例)
坏处:就是复杂度进一步增加,这也是必然的。不过相比带来的好处,复杂度的增加还是可以接受,并克服。
Linux 中对进程的处理抽象成了一个结构体 task_struct
,我前面文章有对这个结构体的介绍。下面就看看进程的内存。
最高位的 1GB 是 linux 内核空间,用户代码不能写,否则触发段错误。下面的 3GB 是进程使用的内存。
Kernel space:linux 内核空间内存 Stack:进程栈空间,程序运行时使用。它向下增长,系统自动管理 Memory Mapping Segment:内存映射区,通过 mmap 系统调用,将文件映射到进程的地址空间,或者匿名映射。 Heap:堆空间。这个就是程序里动态分配的空间。linux 下使用 malloc 调用扩展(用 brk/sbrk 扩展内存空间),free 函数释放(也就是缩减内存空间) BSS 段:包含未初始化的静态变量和全局变量 Data 段:代码里已初始化的静态变量、全局变量 Text 段:代码段,进程的可执行文件
1、未能释放已经不再使用的内存 - 内存泄漏
2、指向不可用的内存指针 - 野指针
3、指针所指向的对象已经被回收了,但是指向该对象的指针仍旧指向已经回收的内存地址 - 悬挂指针
4、分配或释放内存太快或者太慢
5、分配内存大小不合理,造成内存碎片问题
6、内存碎片问题
可以查看前面的文章 TCMalloc 内存分配简析,TCMalloc 内存分配器的原理和 golang 内存分配器原理相近,所以理解了 TCMalloc,golang 内存分配原理也就理解大半,不过 golang 对它也有一些改动。
golang 是怎么解决 二
的内存管理中的常见问题的呢?
针对上面的 1、2、3 这三种问题,golang 使用自动垃圾回收机制,一般情况下,都不使用指针运算(要运算用 unsafe 包),很少的指针使用。当然,内存泄漏问题不能完全根除,但是可以解决一大部分问题。
针对下面的 4、5、6 这三种问题,golang 采用了多级缓存,预分配的方法,来加快内存分配和释放回收,尽量减少内存碎片。详见 TCMalloc 内存分配简析 。
内核已经有一个 malloc 的内存分配器,为什么还有重写一个内存分配器?
可以看到,malloc 是一个很悠久的内存分配器,但是随着时代的发展,多核多线程已经普及,为了更好的应用多线程,提高程序效率,以及改进内存碎片,所以重新写了一个内存分配器。从这里 TCMalloc 内存分配简析 可以看出 TCMaloc 的优点,它将内存划分为多级别,减少锁的开销。而且每个线程的缓存又分开了多个小的对象,以减少内存碎片。等等优化改进。
所以 go 内存分配也继承了这些优点。go 还有一个原因,那就是 go 还有 GC,需要配合内存的垃圾回收。
从上面的进程内存布局图,可以看出一个进程的内存划分了好多不同的区域,而内存管理主要管理的就是 Stack 和 Heap,其中 Stack (栈)区主要由编译器和系统管理,程序语言主要管理 Heap(堆),主要是语言的 runtime 来管理。而且这里的进程内存指的是虚拟内存。
golang 内存分配的基本思想来自 TCMalloc,所以 go 内存分配中的几个概念与 TCMalloc 很相似,可以看看 TCMalloc 中的概念 。
mspan 跟 tcmalloc 中的 span 相似,它是 golang 内存管理中的基本单位,也是由页组成的,每个页大小为 8KB,与 tcmalloc 中 span 组成的默认基本内存单位页大小相同。mspan 里面按照 8*2n 大小(8b,16b,32b .... ),每一个 mspan 又分为多个 object。 就连名字也很像,mspan 中的 m 应该是 memory 的第一个字母。
mcache 跟 tcmalloc 中的 ThreadCache 相似,ThreadCache 为每个线程的 cache,同理,mcache 可以为 golang 中每个 Processor 提供内存 cache 使用,每一个 mcache 的组成单位也是 mspan。
mcentral 跟 tcmalloc 中的 CentralCache 相似,当 mcache 中空间不够用,可以向 mcentral 申请内存。可以理解为 mcentral 为 mcache 的一个 “缓存库”,供 mcaceh 使用。它的内存组成单位也是 mspan。 mcentral 里有两个双向链表,一个链表表示还有空闲的 mspan 待分配,一个表示链表里的 mspan 都被分配了。
mheap 跟 tcmalloc 中的 PageHeap 相似,负责大内存的分配。当 mcentral 内存不够时,可以向 mheap 申请。那 mheap 没有内存资源呢?跟 tcmalloc 一样,向 OS 操作系统申请。 还有,大于 32KB 的内存,也是直接向 mheap 申请。
golang 内存分配几个相关概念,用图来总结一下:
后面再进一步分析 golang 的内存分配原理。
我的另一博客:www.cnblogs.com/jiujuan/p/1…
- 可视化 golang 内存管理
- 《操作系统的设计与实现》
- a-program-in-memory linux 内核分析很棒的文章