深入理解GO语言之内存分配

https://juejin.im/post/59f2e19f5188253d6816d504

前言:开通专栏后的第一篇文章,接下来将会就GO语言的内存,GC,并发编程等,深入理解GO这门语言。

一,内存模型概述

首先明确几个概念:
(1) cache:线程私有的,每次对象分配时候先从cache查询,小对象如果能获得空闲内存则不用加锁了。来看看cache的结构(省略了跟gc等相关的字段)

  1. type mcache struct {
  2. alloc [numSpanClasses]*mspan // 用于分配的span
  3. spanclass spanClass // size class and noscan (uint8)
  4. }

阅读源码我们可以看到cache有一个0到n的数组,每个数组挂载着一个链表,链表的节点代表着一个内存单元,而且同一个链表的节点的内存块都是相等的,不同链表内存大小不同(大小根据spanclass的值作为下标来对应sizeclass.go中的class_to_size数组)。接下来我们看看内存分配在这里的逻辑,我们可以找到malloc.go中的mallocgc(源码就不贴上来了),其实逻辑很简单,源码注释也很清楚,主要是判断对象是否是小对象和大对象(小对象里面还分为tiny和small)。大对象的话直接去堆中分配,小对象根据sizeclass取出一个内存块链表,然后取出该链表的可用节点。对于tiny对象的处理非常有趣,它不能为指针,因为多个tiny对象分配到一个object,无法应对垃圾扫描。

(2) Central:线程共享的,如果在cache中找不到空闲内存,那么cache就会申请一批小对象内存到本地缓存中,这个过程是需要加锁的。(注意:Central里面是一个个的page)结构如下(同样只保留了跟内存相关代码):

  1. type mcentral struct {
  2. spanclass spanClass
  3. nonempty mSpanList // 有空闲内存的span列表
  4. empty mSpanList // 无空闲内存的span列表
  5. }

我们可以通过看malloc.go的mallocgc的代码,我们发现如果cache内存不足,那么会调用到mcache.go的refill函数,再到mcentral.go的cacheSpan函数,然后根据sizeclass大小取出相应的central获取到相应的内存块。在这一层内存管理粒度为span。

(3) Heap:线程共享的,如果Central中没有空闲的内存page,那么就会从Heap中申请内存,这个过程需要加锁。看看结构定义(列出几个重要的跟内存相关的)

  1. type mheap struct {
  2. free [_MaxMHeapList]mSpanList // 页数在127以内的空闲span链表数组
  3. freelarge mTreap // 页数大于127时,转而使用treap
  4. allspans []*mspan // 记录申请过的span
  5. spans []*mspan // 记录arena区域页号跟mspan的映射关系
  6. bitmap uintptr // Points to one byte past the end of the bitmap
  7. bitmap_mapped uintptr
  8. //还有几个跟arena相关的参数就不列举了
  9. }

在这层中,申请内存的单位为page,从heap申请的page是连续的,通过span来管理,这块的逻辑我们可以看mheap.go的alloc_m函数。如果Central向Heap申请内存,那么接下来就会根据page的个数去取最合适的span。接下来盗一个图,觉得画的很详细:

二,内存分配器-Msapn和FixAlloc

这两个都是内存分配器的基础工具组件,在看代码时候我们经常能看到这两个,接下来就分别解释下。

(1) Mspan

这是用来管理page对象的,而且是连续的page,结构定义如下:

  1. type mspan struct {
  2. next *mspan // next span in list, or nil if none
  3. prev *mspan // previous span in list, or nil if none
  4. list *mSpanList // 1.9后计划移除的字段,不做学习
  5. startAddr uintptr // 第一个span的地址
  6. npages uintptr // 该span存储的page个数
  7. }

上面给出了几个重要的字段,可以看出,span结构的next和prev指针是用来构造双向链表的,其实span的用处也只是管理一组连续的page而已,还是比较简单的

(2) FixAlloc

这是用来管理MCache和MSpan的两个特定的对象,结构定义如下:

  1. type fixalloc struct {
  2. size uintptr
  3. first func(arg, p unsafe.Pointer) // called first time p is returned
  4. arg unsafe.Pointer
  5. list *mlink
  6. chunk uintptr
  7. nchunk uint32
  8. inuse uintptr // in-use bytes now
  9. stat *uint64
  10. zero bool // zero allocations
  11. }

list上是一个链表,每个节点是一个固定大小的内存块(cachealloc中的大小为sizeof(MCache),spanalloc的大小为sizeof(MSpan))。接下来我们看到mfixalloc.go中的alloc函数,逻辑大致如下:使用fixalloc分配MCacheMspan时候,那么首先会判断list是否为空,不为空则返回一个内存块使用,如果为空,则判断chunk上有无足够的内存可用,再进行处理

三,总结

写了两天,终于写完了,还是写的很粗糙,相信随着后面的学习,能学习到更多,再回来修改。
老大说过,阅读源码后不能只停留在读源码的层面,要想想读完之后自己的收获,多思考如果是我,会怎么设计。
首先是提高了自己阅读代码的能力吧,然后学习到了treap树,还有就是作者在设计这些内存模型时候的考虑的精妙的思想,例如如何更加快速的计算sizeclass,如何避免False Sharing等问题。有些问题比如FalseSharing是一个很隐蔽的问题,但是确是非常重要的。

ft_authoradmin  ft_create_time2017-11-23 13:39
 ft_update_time2017-11-23 13:41