图解Go内存管理器的内存分配策略
关于Go的内存分配
在Go
语言里,从内存的分配到不再使用后内存的回收等等这些内存管理工作都是由Go
在底层完成的。虽然开发者在写代码时不必过度关心内存从分配到回收这个过程,但是Go
的内存分配策略里有不少有意思的设计,通过了解他们有助于我们自身的提高,也让我们能写出更高效的Go
程序。
Go内存管理的设计旨在在并发环境中快速运行,并与垃圾回收器集成在一起。让我们看一个简单的示例:
package main
type smallStruct struct {
a, b int64
c, d float64
}
func main() {
smallAllocation()
}
//go:noinline
func smallAllocation() *smallStruct {
return &smallStruct{}
}
函数上面的注释//go:noinline
将禁止Go
对该函数进行内联,这样main
函数就会使用smallAllocation
函数返回的指针变量,因为被多个函数使用,返回的这个变量将被分配到堆上。
关于内联的概念之前的文章有说过:
内联是一种手动或编译器优化,用于将简短函数的调用替换为函数体本身。这么做的原因是它可以消除函数调用本身的开销,也使得编译器能更高效地执行其他的优化策略。
所以如果上面的例子不干预编译器的话,编译器通过内联将smallAllocation
函数体里的内容直接放到main
函数里,这样就不会产生smallAllocation
这个函数的调用了,所有的变量都是main
函数内这个范围使用的,也就不在需要将变量往堆上分配了。
继续说上面那个例子,通过逃逸分析命令 go tool compile -m main.go 可以确认我们上面的分析,&smallStruct{}
会被分配到堆上去。
➜ go tool compile -m main.go
main.go:12:6: can inline main
main.go:10:9: &smallStruct literal escapes to heap
借助命令go tool compile -S main.go,可以显示该程序的汇编代码,也可以明确地向我们展示内存的分配:
0x001d 00029 (main.go:10) LEAQ type."".smallStruct(SB), AX
0x0024 00036 (main.go:10) PCDATA $2, $0
0x0024 00036 (main.go:10) MOVQ AX, (SP)
0x0028 00040 (main.go:10) CALL runtime.newobject(SB)
内置函数newobject
会通过调用另外一个内置函数mallocgc
在堆上分配新内存。在Go里面有两种内存分配策略,一种适用于程序里小内存块的申请,另一种适用于大内存块的申请,大内存块指的是大于32KB。
下面我们来细聊一下这两种策略。
小于32KB内存块的分配策略
当程序里发生了32kb
以下的小块内存申请时,Go会从一个叫做的mcache
的本地缓存给程序分配内存。这个本地缓存mcache
持有一系列的大小为32kb
的内存块,这样的一个内存块里叫做mspan
,它是要给程序分配内存时的分配单元。
从mcache中给程序分配内存
在Go的调度器模型里,每个线程M
会绑定给一个处理器P
,在单一粒度的时间里只能做多处理运行一个goroutine
,每个P
都会绑定一个上面说的本地缓存mcache
。当需要进行内存分配时,当前运行的goroutine
会从mcache
中查找可用的mspan
。从本地mcache
里分配内存时不需要加锁,这种分配策略效率更高。
那么有人就会问了,有的变量很小就是数字,有的却是一个复杂的结构体,申请内存时都分给他们一个mspan
这样的单元会不会产生浪费。其实mcache
持有的这一系列的mspan
并不都是统一大小的,而是按照大小,从8字节到32KB分了大概70类的msapn
。
按照大小分类的mspan
就文章开始的那个例子来说,那个结构体的大小是32字节,正好32字节的这种mspan
能满足需求,那么分配内存的时候就会给它分配一个32字节大小的mspan
。
alloc 分配内存
现在,我们可能会好奇,如果分配内存时mcachce
里没有空闲的32字节的mspan
了该怎么办?Go
里还为每种类别的mspan
维护着一个mcentral
。
mcentral
的作用是为所有mcache
提供切分好的mspan
资源。每个central
会持有一种特定大小的全局mspan
列表,包括已分配出去的和未分配出去的。每个mcentral
对应一种mspan
,当工作线程的mcache
中没有合适(也就是特定大小的)的mspan
时就会从mcentral
去获取。mcentral
被所有的工作线程共同享有,存在多个goroutine
竞争的情况,因此从mcentral
获取资源时需要加锁。
mcentral
的定义如下:
//runtime/mcentral.go
type mcentral struct {
// 互斥锁
lock mutex
// 规格
sizeclass int32
// 尚有空闲object的mspan链表
nonempty mSpanList
// 没有空闲object的mspan链表,或者是已被mcache取走的msapn链表
empty mSpanList
// 已累计分配的对象个数
nmalloc uint64
}
mcentral
里维护着两个双向链表,nonempty表示链表里还有空闲的mspan
待分配。empty表示这条链表里的mspan
都被分配了object
。
mcentral
如果上面我们那个程序申请内存的时候,mcache
里已经没有合适的空闲mspan
了,那么工作线程就会像下图这样去mcentral
里去申请。
简单说下mcache
从mcentral
获取和归还mspan
的流程:
- 获取 加锁;从
nonempty
链表找到一个可用的mspan
;并将其从nonempty
链表删除;将取出的mspan
加入到empty
链表;将mspan
返回给工作线程;解锁。 - 归还 加锁;将
mspan
从empty
链表删除;将mspan
加入到nonempty
链表;解锁。
从mcentral里申请mspan
当mcentral
没有空闲的mspan
时,会向mheap
申请。而mheap
没有资源时,会向操作系统申请新内存。mheap
主要用于大对象的内存分配,以及管理未切割的mspan
,用于给mcentral
切割成小对象。
从heap上申请内存
同时我们也看到,mheap
中含有所有规格的mcentral
,所以,当一个mcache
从mcentral
申请mspan
时,只需要在独立的mcentral
中使用锁,并不会影响申请其他规格的mspan
。
上面说了每种尺寸的mspan
都有一个全局的列表存放在mcentral
里供所有线程使用,所有mcentral
的集合则是存放于mheap
中的。mheap
里的arena
区域是真正的堆区,运行时会将 8KB
看做一页,这些内存页中存储了所有在堆上初始化的对象。运行时使用二维的 runtime.heapArena 数组管理所有的内存,每个 runtime.heapArena 都会管理 64MB 的内存。
如果 arena
区域没有足够的空间,会调用 runtime.mheap.sysAlloc 从操作系统中申请更多的内存。
大于32KB内存块的分配策略
Go没法使用工作线程的本地缓存mcache
和全局中心缓存mcentral
上管理超过32KB的内存分配,所以对于那些超过32KB的内存申请,会直接从堆上(mheap
)上分配对应的数量的内存页(每页大小是8KB)给程序。
直接从堆上分配内存
总结
我们把内存分配管理涉及的所有概念串起来,可以勾画出Go内存管理的一个全局视图:
Go内存分配的全局示意图
Go语言的内存分配非常复杂,这个文章从一个比较粗的角度来看Go的内存分配,并没有深入细节。一般而言,了解它的原理,到这个程度也就可以了(应付面试)。
总结起来关于Go内存分配管理的策略有如下几点:
- Go在程序启动时,会向操作系统申请一大块内存,由
mheap
结构全局管理。 - Go内存管理的基本单元是
mspan
,每种mspan
可以分配特定大小的object
。 -
mcache
,mcentral
,mheap
是Go
内存管理的三大组件,mcache
管理线程在本地缓存的mspan
;mcentral
管理全局的mspan
供所有线程使用;mheap
管理Go
的所有动态分配内存。 - 一般小对象通过
mspan
分配内存;大对象则直接由mheap
分配内存。
相关阅读
参考链接
Memory Management and Allocation[1]
图解Go语言内存分配[2]
内存分配器[3]
参考资料
[1]
Memory Management and Allocation: https://medium.com/a-journey-with-go/go-memory-management-and-allocation-a7396d430f44
[2]
图解Go语言内存分配: https://juejin.im/post/6844903795739082760#heading-7
[3]
内存分配器: https://draveness.me/golang/docs/part3-runtime/ch07-memory/golang-memory-allocator/
- 教你快速安装OpenShift容器平台3.6
- 面向开发者的Cloud Foundry
- 云数据库安全与农场和餐馆:知道来源的重要性
- 云数据库安全,农场和餐馆:知道你的来源的重要性
- NO.32 不堪重负:线程池拒绝策略
- 工厂模式进阶之Android中工厂模式源码分析
- C加加游戏编程,大神十年的绝技,正确的入门,这才叫学习
- 我们应该担心吗?人工智能现在可以通过交谈来学习新单词!
- 印度财政部:比特币是纯粹投机行为 区块链资产是“庞氏骗局”
- 法律人工智能实验室成立,法官和律师会丢饭碗吗?
- 让GridView中CheckBox列支持FireFox
- 在ASP.NET MVC中通过URL路由实现对多语言的支持
- AI加持下的假肢将会越来越聪明
- 通过几个Hello World感受.NET Core全新的开发体验
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 【C语言简单说】二:第一个C语言程序详解(3)
- 【C语言简单说】三:整数变量和输出扩展(1)
- 【C语言简单说】三:整数变量和输出扩展(2)
- 【C语言简单说】三:整数变量扩展和输出扩展(3)
- 【C语言简单说】三:浮点数变量和字符变量(4)
- 【C语言简单说】三:变量总结ASCII码扩展(5)
- 【C语言简单说】四:常量
- 【C语言简单说】五:常用运算符
- 【C语言简单说】六:取模运算符以及变量的扩展
- 【C语言简单说】七:自定义函数(1)
- 【C语言简单说】七:自定义函数(2)
- 【C语言简单说】七:自定义函数(3)
- 【C语言简单说】八:分支结构之if(1)
- 【C语言简单说】八:分支结构之if...else...(2)
- 【C语言简单说】八:分支结构之if...else if()...else...(3)