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 类,如下所示:
https://sourcegraph.com/github.com/golang/go@master/-/blob/src/runtime/sizeclasses.go#L83:1
1// class 大小: 0 -> 32K
2var class_to_size = [_NumSizeClasses]uint16{
3 0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192,
4 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512,
5 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792,
6 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144,
7 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568,
8 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768,
9}
10
11// class 所需页数
12var class_to_allocnpages = [_NumSizeClasses]uint8{
13 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
14 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 2, 1, 2, 1,
15 3, 2, 3, 1, 3, 2, 3, 4, 5, 6, 1, 7, 6, 5, 4, 3, 5, 7, 2, 9,
16 7, 5, 8, 3, 10, 7, 4,
17}
例如,对于 1024B 的对象将使用 1 个页(8192B),则将页划分为了 8 个相同大小的区域(1024B):
mspan
mspan 是 go 内存管理的基本单元,也是组织 pages 的数据结构,多个 mspan 组成一个双向链表:
https://sourcegraph.com/github.com/golang/go@619072be4138e3fc092a9b77d57a9abc5333a4ab/-/blob/src/runtime/mheap.go#L402:6
1type mspan struct {
2 next *mspan // next span in list, or nil if none
3 prev *mspan // previous span in list, or nil if none
4
5 startAddr uintptr // address of first byte of span aka s.base()
6 npages uintptr // number of pages in span
7
8 spanclass spanClass // size class and noscan (uint8)
9
10 ...
11}
mcache
mcache 是 go 的线程缓存,与逻辑处理器 P 绑定,每个 mcache 持有 67*2 个 mspan(scan + noscan)(保存在 alloc 中),用来存储微小对象:
https://sourcegraph.com/github.com/golang/go@619072be4138e3fc092a9b77d57a9abc5333a4ab/-/blob/src/runtime/mcache.go
1type mcache struct {
2 alloc [numSpanClasses]*mspan // spans to allocate from, indexed by spanClass
3
4 ...
5}
当 mspan 空间不够时,mspan 会向 mcentral 申请更多的 mspans。
mcentral
mcentral 中心缓存是持有 mspans,包含两个链表:
- empty: 包含空闲对象的链表
- nonempty:不包含空闲对象的链表
访问中心缓存需要锁
https://sourcegraph.com/github.com/golang/go@619072be4138e3fc092a9b77d57a9abc5333a4ab/-/blob/src/runtime/mcentral.go
1type mcentral struct {
2 lock mutex
3 spanclass spanClass
4
5 // For !go115NewMCentralImpl.
6 nonempty mSpanList // list of spans with a free object, ie a nonempty free list
7 empty mSpanList // list of spans with no free objects (or cached in an mcache)
8
9 ...
10}
当 mcentral 空间不足时,mcentral 会向 mheap 申请更多的页用来建立新的 mspan
mheap
mheap 是 go 中管理 heap 的全局唯一结构,它管理着虚拟内存空间
https://sourcegraph.com/github.com/golang/go@619072be4138e3fc092a9b77d57a9abc5333a4ab/-/blob/src/runtime/mheap.go#L73:2
1type mheap struct {
2 // lock must only be acquired on the system stack, otherwise a g
3 // could self-deadlock if its stack grows with the lock held.
4 lock mutex
5 pages pageAlloc // page allocation data structure
6
7 // In general, this is a two-level mapping consisting of an L1
8 // map and possibly many L2 maps. This saves space when there
9 // are a huge number of arena frames. However, on many
10 // platforms (even 64-bit), arenaL1Bits is 0, making this
11 // effectively a single-level map. In this case, arenas[0]
12 // will never be nil.
13 arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
14
15 // central free lists for small size classes.
16 // the padding makes sure that the mcentrals are
17 // spaced CacheLinePadSize bytes apart, so that each mcentral.lock
18 // gets its own cache line.
19 // central is indexed by spanClass.
20 central [numSpanClasses]struct {
21 mcentral mcentral
22 pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
23 }
24
25 ...
26}
mheap 中有两个重要的字段:
- central: 全局中心缓存,共 67 + 67 个
- arenas: 二维矩阵,heap 的内存区域,每个 areana 管理着一系列 8k pages
参考链接
- Go 内存分配器
- A visual guide to Go Memory Allocator from scratch (Golang)
- 深入Linux内核架构
- 虚拟内存