了解了上一篇google的TCMalloc内存原理后 juejin.cn/post/691900… 相信理解Golang内存管理就容易很多
Golang的内存管理的核心思想就是完成类似预分配、内存池等操作,以避开系统调用带来的性能问题,防止每次分配内存都需要系统调用。
下图是Golang内存管理流程图 PS:如果本图有什么错请各位大佬指正
对图中几个名词进行解释
page
mheap向虚拟内存申请的最小单位。一般为8KB
span
go内存分配的基本单位,有n个page组成
class size
为了减少内存碎片,将span的大小分级。目前分为0-66级共67级。可以看到class=0是没有使用的(图中也标为灰色)
66种span如下:(v.14.13)
// 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%
// 11 160 8192 51 32 9.73%
// 12 176 8192 46 96 9.59%
// 13 192 8192 42 128 9.25%
// 14 208 8192 39 80 8.12%
// 15 224 8192 36 128 8.15%
// 16 240 8192 34 32 6.62%
// 17 256 8192 32 0 5.86%
// 18 288 8192 28 128 12.16%
// 19 320 8192 25 192 11.80%
// 20 352 8192 23 96 9.88%
// 21 384 8192 21 128 9.51%
// 22 416 8192 19 288 10.71%
// 23 448 8192 18 128 8.37%
// 24 480 8192 17 32 6.82%
// 25 512 8192 16 0 6.05%
// 26 576 8192 14 128 12.33%
// 27 640 8192 12 512 15.48%
// 28 704 8192 11 448 13.93%
// 29 768 8192 10 512 13.94%
// 30 896 8192 9 128 15.52%
// 31 1024 8192 8 0 12.40%
// 32 1152 8192 7 128 12.41%
// 33 1280 8192 6 512 15.55%
// 34 1408 16384 11 896 14.00%
// 35 1536 8192 5 512 14.00%
// 36 1792 16384 9 256 15.57%
// 37 2048 8192 4 0 12.45%
// 38 2304 16384 7 256 12.46%
// 39 2688 8192 3 128 15.59%
// 40 3072 24576 8 0 12.47%
// 41 3200 16384 5 384 6.22%
// 42 3456 24576 7 384 8.83%
// 43 4096 8192 2 0 15.60%
// 44 4864 24576 5 256 16.65%
// 45 5376 16384 3 256 10.92%
// 46 6144 24576 4 0 12.48%
// 47 6528 32768 5 128 6.23%
// 48 6784 40960 6 256 4.36%
// 49 6912 49152 7 768 3.37%
// 50 8192 8192 1 0 15.61%
// 51 9472 57344 6 512 14.28%
// 52 9728 49152 5 512 3.64%
// 53 10240 40960 4 0 4.99%
// 54 10880 32768 3 128 6.24%
// 55 12288 24576 2 0 11.45%
// 56 13568 40960 3 256 9.99%
// 57 14336 57344 4 0 5.35%
// 58 16384 16384 1 0 12.49%
// 59 18432 73728 4 0 11.11%
// 60 19072 57344 3 128 3.57%
// 61 20480 40960 2 0 6.87%
// 62 21760 65536 3 256 6.25%
// 63 24576 24576 1 0 11.45%
// 64 27264 81920 3 128 10.00%
// 65 28672 57344 2 0 4.91%
// 66 32768 32768 1 0 12.50%
复制代码
bytes/obj 指的是span的大小,可以看到范围是8B~32KB bytes/span 指的是占用堆的字节数,也就是页数*页大小 (eg:8192=1x8k) objects 值得是该span可以分配对象的个数(eg:1024=8192/8) tail waste 产生的内存碎片 (eg:32=8192%48)
mcache
mcache是分配给M运行中的goroutine,是协程级所以无需加锁。为什么不用加锁呢,是因为在M上运行的goroutine只有一个,不会存在抢占资源的情况,所以是无需加锁的。
从上图中可以看到,mache供2种类型的对象分配内存。一个是微对象[1B,16B),一个是小对象[16B,32KB]。
在图中可以看到,对于微对象的内存分配是由mcache提供专门的Tiny allocator专门进行分配,具体的分配流程后续会介绍。
小对象是选择最适应自己大小的span进行分配,从图中可以可以看到同一级别的span是分成2类的,一类是可以被GC扫描的span,里面是包含指针的对象;另一类似不可以被GC扫描的span,里面不包含指针的对象。可以看到分配内存的时候会按照是否有指针对象对应不同的span,为了后续GC垃圾回收使用。
每一个级别的span链表是一个双向链表,每一个span都会指向前一个span和后一个span。每一级size class可以有1个或者多个span
当小对象申请内存在mache不够时,会继续向mcentral进行申请
mcentral
mcentral是为mcache提供切分好的span。mcentral是全局的,也就是多个M共享mcentral,会出现并发问题,所以此时申请都是需要加锁的。
mcentral存储67级别大小span,其中size=0是不使用的(图中标灰色)。每一级别的span分为2种,一种empty表示这条链的mspan已经被分配了对象,或者已经被mcache使用,被对应线程占用;nonempty表示有空闲对象的 mspan列表
值得注意的是mcentral链表都在mheap进行维护
若分配内存是没有空闲的span的列表,此时需要像mheap申请。
mheap
mheap是go程序持有的整个堆空间,是go的全局变量,所以在使用的时候需要全局锁。
大对象(大于32KB)直接通过mheap进行分配。除此之外,mcentrals保存在mheap中,mheap对mcentral了如指掌。
若mheap没有足够的内存,则会向虚拟内存申请page,然后将page组装成span再供程序使用。
mheap还存储多个heapArena ,heapArena 存储连续的span,主要是为了mheap管理span和GC垃圾回收
微对象 [1B,16B)
微对象的内存分配是由mcache提供专门的Tiny allocator专门进行分配,分配的对象是不包含指针的,例如一些小的字符串和不包含指针的独立逃逸变量等。
小对象 [16B,32KB]
小对象是在mache申请适合自己大小的span,若mache没有可用的span,mache会向mcentral申请,加锁,找一个可用的span,从nonempty删除该span,然后放到empty链表中,将span返回给工作线程,解锁;若没有足够的内存,mcentral还会继续向mheap继续申请。
当归还时,加锁,将empty链表删除对应的span,然后将其加到nonempty链表中,解锁。
大对象(32KB,+∞)
大对象,使用mheap直接分配,若mheap没有足够的内存,则mheap向虚拟内存申请若干个pages。可以看到,约到后面申请内存的代价就越来越大
回头看这样的内存管理的优点是什么
1、可以看到申请内存的时候是以span为单位的,span又分为不同大小,从大小的规律我们可以看到不是简单的按照2次幂进行递增的,是根据计算造成碎片最少的情况下对span的分类,在申请的时候会减少内存碎片。比如在申请47B大小的时候,如果按照2次幂会提供64B大小的内存供应用使用,但是如果按照span会提供48B大小的span,很明显看出,后者造成的碎片会更少。
2、每次从操作系统申请一大块内存,由Go来做分配,减少了系统调用
3、go的内存算法是使用google的TCMalloc内存管理算法,把内存分的非常细,分为多级管理,减少锁的粒度。在回收对象内存时,并没有将其真正释放掉,只是放回预先分配的大块内存中,以便复用。只有内存闲置过多的时候,才会尝试归还部分内存给操作系统,降低整体开销
阅读完本文,相信你对golang的内存管理有了清楚的认识。下一篇我们来看看源码是怎么实现的。