分页存储
分页存储管理是操作系统中重要的内容,其核心思想是将进程的地址空间分为若干个固定大小的页,每页称为一个“页框”,或称“页帧”、“内存块”、“物理块”。每个页框都有一个编号,即“页框号”。同时,将虚拟和物理内存空间划分为同样大小的页和帧,并建立映射方案,实现物理地址和逻辑地址的转换。这种存储方式可以提高内存交换效率,减少内存碎片。但也会增加页表的存储空间和数据复制的成本,可能出现抖动现象
虚拟地址
在计算机系统中,虚拟地址空间是由操作系统管理的,当一个用户进程被创建时,操作系统会为其分配一段连续且独立的虚拟地址空间都从0开始,进程之间相互隔离。用户进程可以通过访问连续的虚拟地址来访问物理内存中的不同数据和指令。
内存分页
操作系统使用内存分页技术。内存被分割成大小相等的块,称为页。每个进程的虚拟地址空间也被分成大小相等的块,称为虚拟页。操作系统使用页表来映射虚拟页到物理页(实际内存中的页)的对应关系。每个进程的页表都是独立的,使得不同进程的虚拟页可以映射到不同的物理页,实现了内存隔离。
内存保护
为了防止进程之间相互干扰和访问彼此的数据,操作系统通过内存保护机制来限制每个进程对内存的访问。在页表中,可以设置页的访问权限,比如只读、可写、可执行等。操作系统会根据进程的访问权限来设置页表项,确保进程只能访问自己所拥有的内存区域,而不能访问其他进程的内存。
进程切换
在多道程序环境下,CPU会在不同的进程之间进行切换,以实现并发执行。当一个进程被调度执行时,操作系统会将该进程的页表装载到内存管理单元(MMU)中,从而使得进程的虚拟地址能正确映射到物理地址。当进程切换时,MMU会切换到下一个进程的页表,从而实现进程间内存隔离。
内存分配
这次讨论的内存分配主要是堆区内存分配,Golang 采取内存预分配策略,page作为组成堆区mheap的基本单位,每个page大小8k,若干个page组成一个mspan,mspan一共有68种规格之所以将mspan划分为不同规格主要解决减少内存碎片,提高内存的利用率
// class bytes/obj bytes/span objects tail waste max waste min align
// 1 8 8192 1024 0 87.50% 8
// 2 16 8192 512 0 43.75% 16
// 3 24 8192 341 8 29.24% 8
// 4 32 8192 256 0 21.88% 32
// 5 48 8192 170 32 31.52% 16
// 6 64 8192 128 0 23.44% 64
// 7 80 8192 102 32 19.07% 16
// 8 96 8192 85 32 15.95% 32
// 9 112 8192 73 16 13.56% 16
// 10 128 8192 64 0 11.72% 128
// 11 144 8192 56 128 11.82% 16
// 12 160 8192 51 32 9.73% 32
// 13 176 8192 46 96 9.59% 16
// 14 192 8192 42 128 9.25% 64
// 15 208 8192 39 80 8.12% 16
// 16 224 8192 36 128 8.15% 32
// 17 240 8192 34 32 6.62% 16
// 18 256 8192 32 0 5.86% 256
// 19 288 8192 28 128 12.16% 32
// 20 320 8192 25 192 11.80% 64
// 21 352 8192 23 96 9.88% 32
// 22 384 8192 21 128 9.51% 128
// 23 416 8192 19 288 10.71% 32
// 24 448 8192 18 128 8.37% 64
// 25 480 8192 17 32 6.82% 32
// 26 512 8192 16 0 6.05% 512
// 27 576 8192 14 128 12.33% 64
// 28 640 8192 12 512 15.48% 128
// 29 704 8192 11 448 13.93% 64
// 30 768 8192 10 512 13.94% 256
// 31 896 8192 9 128 15.52% 128
// 32 1024 8192 8 0 12.40% 1024
// 33 1152 8192 7 128 12.41% 128
// 34 1280 8192 6 512 15.55% 256
// 35 1408 16384 11 896 14.00% 128
// 36 1536 8192 5 512 14.00% 512
// 37 1792 16384 9 256 15.57% 256
// 38 2048 8192 4 0 12.45% 2048
// 39 2304 16384 7 256 12.46% 256
// 40 2688 8192 3 128 15.59% 128
// 41 3072 24576 8 0 12.47% 1024
// 42 3200 16384 5 384 6.22% 128
// 43 3456 24576 7 384 8.83% 128
// 44 4096 8192 2 0 15.60% 4096
// 45 4864 24576 5 256 16.65% 256
// 46 5376 16384 3 256 10.92% 256
// 47 6144 24576 4 0 12.48% 2048
// 48 6528 32768 5 128 6.23% 128
// 49 6784 40960 6 256 4.36% 128
// 50 6912 49152 7 768 3.37% 256
// 51 8192 8192 1 0 15.61% 8192
// 52 9472 57344 6 512 14.28% 256
// 53 9728 49152 5 512 3.64% 512
// 54 10240 40960 4 0 4.99% 2048
// 55 10880 32768 3 128 6.24% 128
// 56 12288 24576 2 0 11.45% 4096
// 57 13568 40960 3 256 9.99% 256
// 58 14336 57344 4 0 5.35% 2048
// 59 16384 16384 1 0 12.49% 8192
// 60 18432 73728 4 0 11.11% 2048
// 61 19072 57344 3 128 3.57% 128
// 62 20480 40960 2 0 6.87% 4096
// 63 21760 65536 3 256 6.25% 256
// 64 24576 24576 1 0 11.45% 8192
// 65 27264 81920 3 128 10.00% 128
// 66 28672 57344 2 0 4.91% 4096
// 67 32768 32768 1 0 12.50% 8192
内存布局
下图展示Golang进程的虚拟内存空间布局,整个堆区
mheap组成包含arenas、central、mcache,它们都有自己的数据结构管理各自内存空间,对内存合理组织无论是在提升内存利用率还是减少内存碎片或者gc垃圾回收等方面
mheap数据结构
type mheap struct {
//全局锁
lock mutex
//页面分配的数据结构
pages pageAlloc
//所有的mspan结构指针
allspans []*mspan
.......
//heapArena结构指针数组
arenas [1 << arenaL1Bits]*[1 << arenaL2Bits]*heapArena
.......
//当前区的开始和结束地址
curArena struct {
base, end uintptr
}
//mcentral结构数组
//在Go语言的内存管理中,mspan是最小的内存分配单元,而mcentral则是全局的mspan管理者,负责为所有mcache提供所有线程的内存管理
//numSpanClasses的值是由Go语言运行时(runtime)预定义的一个常量,其值为68*2=136。这个值实际上代表了mspan的类别数,每个类别对应着不同大小的内存块。具体来说,Go语言将mspan分为了67个不同的类别,每个类别的大小都是2的幂次方,从32KB到1GB不等。其中,第0号和第136号类别是保留类别,不用于实际的内存分配。因此,实际上可用的mspan类别数为68个
//在这68个可用的mspan类别中,有68个是需要扫描的中心缓存(即包含指针的内存块),另外的68个则是不需要扫描的中心缓存(即不包含指针的内存块)。需要扫描的中心缓存会在垃圾回收时被扫描,以查找未被引用的对象并释放它们占用的内存。而不需要进行扫描的中心缓存则可以直接释放它们占用的内存,因为它们不包含任何指针
//通过将mspan划分为不同的类别,Go语言可以更加高效地管理和分配内存,避免了内存碎片的问题,并提高了内存使用的效率
central [numSpanClasses]struct {
mcentral mcentral
pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
}
.......
}
arenas数据结构
type heapArena struct {
// 位图,详见下文
bitmap [heapArenaBitmapBytes]byte
//是一个8192(pagesPerArena)大小的指针数组,每个mspan对应8KB
//这是只是表示有这么多mspan,并不是指一个mspan只有一个page
//在mheap分配内存的时候可能n个page对应一个mspan,会在mheap_.alloc.allocSpan.setSpans中将这个mspan的指针,对应到heapArena.spans的n个位置
spans [pagesPerArena]*mspan
//表示page是否被使用 = 8192 / 8 = 1024
pageInUse [pagesPerArena / 8]uint8
//页是否被标记,gc使用
pageMarks [pagesPerArena / 8]uint8
//又是一个与pageInUse类似的位图,只不过标记的是哪些span包含特殊设置,目前主要指的是包含finalizers,或者runtime内部用来存储heap profile数据的bucket。
pageSpecials [pagesPerArena / 8]uint8
//一个大小为1MB的位图,其中每个二进制位对应arena中一个指针大小的内存单元。当开启调试debug.gccheckmark的时候,checkmarks位图用来存储GC标记的数据。该调试模式会在STW 的状态下遍历对象图,用来校验并发回收器能够正确地标记所有存活的对象。
checkmarks *checkmarksMap
// 记录的是当前arena中下个还未被使用的页面的位置,相对于arena起始地址的偏移量。页面分配器会按照地址顺序分配页面,所以zeroedBase之后的页面都还没有被用到,因此还都保持着清零的状态。通过它可以快速判断分配的内存是否还需要进行清零
zeroedBase uintptr
}
mcentral数据结构
//go:notinheap
type mcentral struct {
spanclass spanClass //当前mcentral是哪一种spanclass
//在Go语言中,内存管理是通过垃圾回收器(Garbage Collector)来自动处理的。垃圾回收器会定期扫描内存中的 span,并释放不再被引用的对象所占用的内存。
//partial [2]spanSet 和full [2]spanSet是用于跟踪内存使用情况的两个列表。它们分别表示具有空闲对象和没有空闲对象的spanSet。
//partial [2]spanSet:这个列表包含了一些span,这些span中仍然有未被释放的对象。这些对象可能是由于程序逻辑错误或延迟释放导致的。通过跟踪这些span,可以识别出潜在的内存泄漏问题,并进行相应的修复。
//full [2]spanSet:这个列表包含了一些span,这些span中的所有对象都已经被释放,没有任何剩余的对象需要管理。这意味着这些跨度已经完全清空,可以被安全地回收。
//通过设置这两个列表,可以更好地了解内存的使用情况,并及时发现和解决潜在的内存问题。
partial [2]spanSet // 有可用空间的span集合
full [2]spanSet // 没可用空间的span集合 或者当前链表里的Span已经交给mcache
}
mcache数据结构
//go:notinheap
type mcache struct {
nextSample uintptr // 堆分析的下一个采样
scanAlloc uintptr // 用来指示已分配堆的扫描情况
// 微对象分配相关,详见微对象分配
tiny uintptr
tinyoffset uintptr
tinyAllocs uintptr
alloc [numSpanClasses]*mspan // 136个span链表
stackcache [_NumStackOrders]stackfreelist //栈相关
flushGen uint32
}
如何分配mcache
//1.首先,定义了一个指向mcache类型的指针变量c。
//2.然后,调用systemstack函数,这是一个用于在系统堆栈上执行操作的函数。在这个函数中,首先获取了mheap_的锁,然后从mheap_.cachealloc中分
//3.配了一块内存,并将其转换为mcache类型,赋值给c。同时,将mheap_.sweepgen赋值给c.flushGen。最后,释放了mheap_的锁。
//4.接下来,遍历c.alloc数组,将其每个元素设置为&emptymspan。这可能是为了初始化或重置这个数组。
//5.然后,调用nextSample()函数,并将返回值赋给c.nextSample。这可能是为了设置或更新内存缓存的下一个样本。
//6.最后,返回c。
//这段代码的主要目的是创建一个新的内存缓存,并对其进行初始化和配置。
func allocmcache() *mcache {
var c *mcache
systemstack(func() {
lock(&mheap_.lock)
c = (*mcache)(mheap_.cachealloc.alloc())
c.flushGen = mheap_.sweepgen
unlock(&mheap_.lock)
})
for i := range c.alloc {
c.alloc[i] = &emptymspan
}
c.nextSample = nextSample()
return c
}
mspan数据结构
//go:notinheap
type mspan struct {
// 前后指针,分别指向了前后的Span
next *mspan
prev *mspan
// 当前Span的第一个page的首地址
startAddr uintptr
// 代表当前Span是由多少Page构成的 startAddr*npages*pgae size(8KB)就是当前span分配空间的大小
npages uintptr
manualFreeList gclinkptr // 空闲对象列表
// freeindex是0~nelems的位置索引, 标记当前span中下一个空对象索引
freeindex uintptr
nelems uintptr // 当前span中管理的对象数
allocCache uint64 // 从freeindex开始的位标记
allocBits *gcBits // 该mspan中对象的位图
gcmarkBits *gcBits // 该mspan中标记的位图,用于垃圾回收
spanclass spanClass // 当前span 对应的spanclass
.......
}