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

wacxt · 2017-10-30 01:16:08 · 1460 次点击 · 预计阅读时间 3 分钟 · 大约8小时之前 开始浏览    
这是一个创建于 2017-10-30 01:16:08 的文章,其中的信息可能已经有所发展或是发生改变。

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

一,内存模型概述

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

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

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

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

type mcentral struct {
    spanclass spanClass
    nonempty  mSpanList // 有空闲内存的span列表
    empty     mSpanList // 无空闲内存的span列表
}

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

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

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

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

二,内存分配器-Msapn和FixAlloc

这两个都是内存分配器的基础工具组件,在看代码时候我们经常能看到这两个,接下来就分别解释下。
(1) Mspan
这是用来管理page对象的,而且是连续的page,结构定义如下:

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

上面给出了几个重要的字段,可以看出,span结构的next和prev指针是用来构造双向链表的,其实span的用处也只是管理一组连续的page而已,还是比较简单的
(2) FixAlloc
这是用来管理MCache和MSpan的两个特定的对象,结构定义如下:
type fixalloc struct {
    size   uintptr
    first  func(arg, p unsafe.Pointer) // called first time p is returned
    arg    unsafe.Pointer
    list   *mlink
    chunk  uintptr 
    nchunk uint32
    inuse  uintptr // in-use bytes now
    stat   *uint64
    zero   bool // zero allocations
}

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

三,总结

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


有疑问加站长微信联系(非本文作者)

本文来自:掘金

感谢作者:wacxt

查看原文:深入理解GO语言之内存分配

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1460 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传
X
登录和大家一起探讨吧