Go 内存管理,内存分配
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
内存管理
内存管理是一个古老的话题C/C++这类语言需手动管理堆内存的申请与释放。Go、Java这类带有垃圾回收器GC的语言堆内存的申请与释放可以交给其运行时来完成。Rust这种新兴语言通过编译器确定内存管理分配与回收方式其不需要手动管理内存也不需要垃圾回收器它是将对象的生命周期限定在作用域内对象生命周期超出作用域自动执行Drop方法来销毁对象这是编译器指定的行为。
Go内存管理
Go语言的内存管理可以分为四个阶段分别是
- 向操作系统申请内存由(Page Allocator)完成
- runtime为程序分配内存由(Object Allocator)完成
- runtime为程序做垃圾回收由(Garbage Collector)完成
- 向操作系统归还内存由(Scavenger)完成
向操作系统申请内存和归还内存调用其syscall即可。操作系统会为应用程序做虚拟内存与物理内存的映射之后返回虚拟内存的空间。
堆与栈
Go语言淡化了堆与栈的概念用户无法直接操作Go运行时的堆栈。Go程序的中操作系统为进程划分的栈空间为Go runtime所用同时堆空间被分为了两部分即runtime所使用的堆和Go 用户态代码所使用的堆同时goroutine的栈也是从Go用户代码所使用的堆中进行分配的这使得goroutine的栈可以"任性"扩容或缩容。下图描述了其大概原理。goroutine所使用的堆栈和传统的堆栈使用方法类似栈上的空间会随着方法的执行结束而被回收堆上的空间多数情况下由GC代为管理但Go 1.20之后加入的Arena特性可以将"部分堆空间"的管理权限交给用户这也是Go内存管理方式的改进。
os stack
-------------------
| |
| runtime |
| |
-------------------
|
|
os heap |
------------------------------------------
| | |
| | Go heap | -------------
| runtime | | | stack |
| | user code | | goroutine |
| | |------------->-------------
------------------------------------------
runtime 所占的堆空间可以称之为堆外内存而用户代码持有的堆空间可以称之为Go堆。
三级内存对象管理
Go运行时对堆内存对象管理做了三级划分分别用mheapmcentralmcache表示不同的层级mheap是保存所有内存对象的结构mheap中有一个central字段其类型如下:
central [numSpanClasses]struct {
mcentral mcentral
pad [cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
}
可见为数组类型数组的长度为nSpanClasses该值在Go 1.19.3版本中为68 << 1 = 136。central数组根据下标划分保存了不同级别的central不同级别的central中保存了该级别的mspan链表mspan是Go管理堆内存的基本单元其代表被分配的内存(相连的页)。mspan级别被分为67级以下是Go1.19.3源码中的注释位于src/runtime/sizeclasses.go文件中
// 级别 单个内存占用 总内存占用 分配对象个数 浪费字节数 最大浪费率 最小对齐方式
// 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
如下介绍浪费率的计算方式单个65级span占用字节数为27264为该span级别分配的总内存是8192081920/27264 = 381920 % 27264 = 128所以在分配三个span后还剩余128字节无法被分配回看单个64级span其占用字节数为24576多疑当需要一个24577字节内存的span时会分配65级的span。65级的span的浪费率的计算公式是((27264 - 24576 + 1) * 3 + 128/ 81920 = 0.999 ≈ 10 %。
在GMP调度模型中mcentral被所有的P共享同时P中有一个称之为mcache的span缓存。当P绑定的M执行G的时候需要使用内存则去mcache缓存中去获取若可以获取则拿来使用若无法获取则去mcentral中去寻找。mcache可以说是一个二级缓存。
Go内存分配策略
Go 内存分配策略可以分为顺序分配和自由表分配两种前者是连续的一片内存区域前边的内存区域是已被分配的内存后边的内存区域是未被分配的内存后者使用链表链接起来不同的未被分配的内存区域。前者对CPU的空间局部性十分友好但容易产生内存碎片不太便于管理。后者十分灵活。在Go语言中用自由表分配策略维护堆外空间的内存分配用顺序分配维护Go堆的内存分配用户侧代码。
总结
Go内存分配部分演进的比较挫折早期Go版本这方面逻辑非常粗糙。后期得以改进。在内存管理中如果需要超大的内存空间则有mheap单独向操作系统申请mheap中维护了代表内存使用状态的位图使用radix tree管理线性的地址空间。
Reference
《Go 语言底层原理剖析》
《Go 程序员面试笔试宝典》
Go 1.19.3 runtime源码