内存管理
1. 简介
程序要运行,离不开内存的分配;内存作为计算机中稀缺资源,内存管理的好坏直接决定程序的优劣。 在编程语言中内存的管理分为两类:
- 程序员手动管理(诸如C、C++)
- 由编程语言自己管理(比如,python、ruby、go等)
我们总是希望程序能够占用尽量少的内存,实现尽量高的性能;因此无论是操作系统层面,还是编程语言层面都做了许多的努力。
当然go语言为了实现这个目标,对内存的使用和回收,也是精打细算,做了很多优化,下面我们一起来看看。
2. 程序内存该如何分配才好?
假设我们是一门程序语言的实现者,要实现一个高性能的内存管理语言,我们不禁要问,什么样的内存分配才是好的分配?
首先,内存的申请需要通过操作系统的系统调用实现,而系统调用是有开销的;如果我们每次需要用一点内存,都去通过操作系统系统调用,那么开销可想而知,因此我们不希望频繁的向操作系统申请内存,最好一次申请一大块。
另外,进程拿到一大块内存后,是交给多个线程使用的,也就是都有使用权,在某一个时刻线程A和线程B都需要内存,为了避免他们拿到同一个内存地址,那么此时需要对整块内存加锁。
有了竞争,然后有了锁,既然有锁那么就有开销,我们该如何减少锁的开销呢?
您可以继续思考下去...
经过前面的思考,相信你已经对内存的分配有了一个感官的认识,下面我们看看go是怎么做的。
3. go内存分配框架
go的内存管理是基于TCMalloc(Thread Cache Malloc)核心思想实现的,那什么是TCMalloc呢?
每个线程会维护一个线程内存缓存(ThreadCache),从而减少直接向上层(CentralCache)获取时的锁竞争,每个线程需要内存时优先从线程缓存中获取,由于ThreadCache是每个线程独享的,此时无需加锁;如果ThreadCahe不足,则会从CentralCache获取,centralCache是所有缓存共享的,因此此时需要加锁。
go在借鉴TCMallco的同时做了进一步细化,总体上分了三层:mcache、mcentral、mheap,总体结构如下:
Mcache、Mcentral、MHeap粒度依次由小到大
-
Mcache 和GMP模型中的P绑定,每个P都有一个Mcache,因此每个Goroutine运行时优先从Mcache中获取内存,如果Mcache中内存不足,才向上级(Mcentral)申请,直接在Mcache中获取内存无需加锁
-
Mcentral Mcentral介于Mcache和Mheap之间,当Mcache中内存不够时从Mcentral申请;Mcentral中内存不够时向Mheap申请,Mcentral按照不同的对象大小刻度(比如:8B、16B、32B...)做了区分,因此从指定大小的Mcentral申请内存,只需要锁定对应对象大小的Mcentral就行
-
MHeap MHeap是go内存管理的最大粒度层,它是全局共享的;当Mcentral中没有足够内存时,会向MHeap中获取,此时加的锁也最大;如果Mheap中内存不够,则会向操作系统申请,发起系统调用。
另外,并非是所有大小内存的分配都需要按照上面的层级逐层申请,有时候是可以跨越层级的。
在go中将对象的大小分为三个层级:
- tiny微对象 < 16B ------ 直接在Mcache中分配
- 小对象 16B ~ 32KB ------ 逐层走流程正常申请
- 大对象 > 32KB ------ 直接到Mheap中申请(跳过Mcentral
之所以这样设计也很好理解,微对象在Mcache中做了专门的处理,目的是减少内存的浪费;大对象直接从Mheap,这样效率会更高。
4. size_class、mspan、object
前面我们从整体层的角度认识了go内存分配,但是缺乏对内存分配细节的把控,这里我们进一步拿出放大镜看看go内存基本单位mspan。
Mcache和Mcentral中都有mspan,那什么是mspan呢? 在go中也像操作系统内存中page的概念,只不过含义不同,一个page的大小为8KB,一个mspan由整数个page组成。
这很好理解,画出来大概这样。
从这么看,page就是最小的组成了,真的是这样么? No,在go中实际上会根据刻度大小------size_class来划定每个mspan的object大小。
我们假定这个mspan就一个page,那么表示出来应该是这样的。
size_class依次为8B、16B、32B、48B、64B...总共67种规格,当需要分配内存时,选择对应的规格取出object使用即可。
因此,P与Mcache内存交换单位是object,而Mcentral和Mheap之间内存交换单位是Mspan。
我来看一眼size_class表,有个影响即可
go
// 这只是一部分
// class - 也就size_class
// bytes/object object对象大小
// bytes/span span的大小
// objects 有多少个object
// tail waste span被分配为指定规格object会有多少内存浪费
// max waste 最大浪费
// class bytes/obj bytes/span objects tail waste max waste
// 1 8 8192 1024 0 87.50%
// 2 16 8192 512 0 43.75%
// 3 32 8192 256 0 46.88%
// 4 48 8192 170 32 31.52%
// 5 64 8192 128 0 23.44%
// 6 80 8192 102 32 19.07%
// 7 96 8192 85 32 15.95%
// 8 112 8192 73 16 13.56%
// 9 128 8192 64 0 11.72%
// 10 144 8192 56 128 11.82%
前面我们已经了解了mspan、object、page、size_class这些概念,但是没有结合Mcache看看,下面我们一起看下它们整体是如何搭配的。
5. 总结
总体来看,go对内存的管理非常精细
- 通过分级缓存的方式,极大的减少锁的竞争;
- 通过Mcache和P绑定的方式,一个goroutine需要内存直接从Mcache中获取提高内存访问局部性。
- 利用size_class, 将内存细化为不同的规格大小,极大的减少内存碎片,提高内存分配效率。
参考资料: