Golang 的内存管理本质上就是一个内存池,只不过内部做了很多的优化。比如自动伸缩内存池大小,合理的切割内存块等等
内存池
golang的程序在启动初,会一次性向操作系统申请一大块内存,这块内存空间会放在一个叫 mheap 的 struct 中管理,mheap负责将这一整块内存分割成不同部分使用,并将其中一部分分割成合适大小分配给用户使用。
概念
page 内存页
span 内存块,一个或多个连续的 page 组成一个 span
object 对象,用来存储一个变量数据,一个span会被分割成相同大小的object
假设 object 的大小是 16B,span 大小是 8K,那么就会把 span 中的 page 就会被初始化 8K / 16B = 512 个 object
mheap.spans:用来存储 page 和 span 信息,比如一个 span 的起始地址是多少,有几个 page,已使用了多大等等。
mheap.bitmap 存储着各个 span 中对象的标记信息,比如对象是否可回收等等。
mheap.arena_start: 将要分配给应用程序使用的空间。
分配相同或同一范围的大小的内存时,会根据大小去寻找一个sizeclass,span有不同种类,使用sizeclass进行区分,相同sizeclass的span会以链表形式连在一起。
找到合适的span后会从这个span中找到一个object返回给上层。
mcentral
mheap在初始化时会生成一个大span,如果有需求,会将这个大span切出一个小span放入mcentral进行管理,大 span 由 mheap.freelarge 和 mheap.busylarge 等管理
。如果mcentral不够用了会从mheap.freelarge再切一块。如果 mheap.freelarge 空间不够,会再次从 OS 那里申请内存。
mcache
在mcentral中有一个锁,是为了防止并发情况下申请内存的情况,但是加锁,释放锁,等待锁是一件很浪费时间的事情,所以golang为每一个P申请一个mcache,每个P在申请内存的时候会先从mcache中申请,如果申请不到再从mcentral中申请。从 mcache 上分配内存空间是不需要加锁的,因为在同一时间里,一个 P 只有一个线程在其上面运行,不可能出现竞争。没有了锁的限制,大大加速了内存分配。
优化
zero size:有些对象的大小为0,例如[0]int, struct{},golang会统一返回一个固定的内存地址
package main
import (
"fmt"
)
func main() {
var (
a struct{}
b [0]int
c [100]struct{}
d = make([]struct{}, 1024)
)
fmt.Printf("%p\n", &a)
fmt.Printf("%p\n", &b)
fmt.Printf("%p\n", &c)
fmt.Printf("%p\n", &(d[0]))
fmt.Printf("%p\n", &(d[1]))
fmt.Printf("%p\n", &(d[1000]))
}
// 运行结果,6 个变量的内存地址是相同的:
0x1180f88
0x1180f88
0x1180f88
0x1180f88
0x1180f88
0x1180f88
tiny object 在object <= 8B时,会使用sizeclass = 1的span,一般数据类型为 int32,byte, bool以及一些小对象,但是这样可能会导致并不能完全利用8B而导致了浪费并出现了大量的内存碎片。golang在这里做了优化,在 <=16B时,golang都会从 sizeclass=2 的 span 中获取一个 16B 的 object 用以分配,并且将多个对象复用这个16B的object
大对象:sizeclass最大的span的大小为32K,如果超过这个大小,golang会直接绕过 mcache 和 mcentral,直接从 mheap 上获取,即mheap的freelarge。
内存池的优点:
不需要频繁申请内存了,而是从对象池里拿,程序不会频繁进入内核态。
因为一次性申请一个连续的大空间,对象池会被重复利用,不会出现碎片。
程序频繁访问的就是对象池背后的同一块内存空间,局部性良好。
有疑问加站长微信联系(非本文作者)