1、虚拟内存、物理内存和页表
每一个进程都有自身的虚拟内存,通过页表将虚拟内存映射到物理内存。虚拟内存空间被内核划分为了等长的部分,这部分称之为页,物理内存也被划分为了同样大小的页,通常称之为页帧。
不同的虚拟内存空间中的页可以映射到同一个物理内存页,内核可以决定哪些内存区域在进程间共享,哪些不共享;不是所有的虚拟空间页都会映射到物理内存,虚拟空间远大于物理内存。
用来将虚拟地址空间映射到物理地址空间的数据结构称为页表,实际中为了提高性能、节省空间往往采用多级页表,linux 采用了四级页表。
虚拟内存中的栈主要保存函数的参数、返回值和局部变量等,由编译器管理;用户主动申请的内存主要保存在堆中,由用户和编译器共同管理,并且堆中内存需要垃圾回收机制,java、go、python等语言本身会负责垃圾回收,c、c++等需要用户自己负责内存的申请和释放。
2、简单的内存分配方法
- 线性分配
维护一个指向内存的指针,移动指针实现内存的分配
实现复杂度低,内存分配快,但需要不停拷贝、合并内存以实现内存释放和回收
- 链表分配器
维护一个空闲内存的链表
实现复杂度低,内存的释放和回收简单,但是需要遍历链表,为了进一步提高效率,可以把内存分割成不同的大小,分别组成不同的链表,根据申请内存的大小使用相应的链表
3、TCMalloc
线程缓存分配(TCMalloc,thread-caching malloc)是一种快速、高效的内存分配方法,其核心思想是将内存划分为不同的级别,以减少锁竞争,TCMalloc 将内存划分为了 线程缓存(thread-cache)和页堆(page-heap)两个部分。
Thread Cache
每个线程都拥有一个线程缓存,不需要锁,线程缓存由不同的固定大小链表组成,根据对象的大小选择合适的链表分配内存,减少空间碎片化,小于 32K 的内存将在线程缓存分配。
Page Heap
对于超过 32K 的内存将由页堆直接分配,每个页堆包含不同大小的页集合,span 表示一组连续的页,包含页的起始地址(start)和页数(n_pages)。当页堆空间不足时将向操作系统申请更多的内存空间。
4、GO 内存分配
Go 的内存分配方式借鉴了 TCMalloc,根据对象的大小将对象分成了微对象、小对象和大对象三种:
| 类别 | 大小 |
| ------ | :---------- |
| 微对象 | (0, 16B) |
| 小对象 | [16B, 32KB] |
| 大对象 | (32KB, +∞) |
其中微对象使用微对象内存分配器,小对象使用线程缓存和中心缓存,大对象使用页堆,具体,Go 将 32K 大小的内存对象划分为了 67 类,如下所示:
例如,对于 1024B 的对象将使用 1 个页(8192B),则将页划分为了 8 个相同大小的区域(1024B):
mspan
mspan 是 go 内存管理的基本单元,也是组织 pages 的数据结构,多个 mspan 组成一个双向链表:
mcache
mcache 是 go 的线程缓存,与逻辑处理器 P 绑定,每个 mcache 持有 67*2 个 mspan(scan + noscan)(保存在 alloc 中),用来存储微小对象:
当 mspan 空间不够时,mspan 会向 mcentral 申请更多的 mspans。
mcentral
mcentral 中心缓存是持有 mspans,包含两个链表:
- empty: 包含空闲对象的链表
- nonempty:不包含空闲对象的链表
访问中心缓存需要锁
当 mcentral 空间不足时,mcentral 会向 mheap 申请更多的页用来建立新的 mspan
mheap
mheap 是 go 中管理 heap 的全局唯一结构,它管理着虚拟内存空间
mheap 中有两个重要的字段:
- central: 全局中心缓存,共 67 + 67 个
- arenas: 二维矩阵,heap 的内存区域,每个 areana 管理着一系列 8k pages