Golang 内存管理

原文链接[http://legendtkl.com/2017/04/02/golang-alloc/]
Golang 的内存2861077989管理基于 tcmalloc,可以说起点挺高的。但是 Golang 在实现的时候还做了很多优化,我们下面通过源码来看一下 Golang 的内存管理实现。下面的源码分析基于 go1.8rc3。
关于 tcmalloc 可以参考这篇文章 tcmalloc 介绍,原始论文可以参考 TCMalloc : Thread-Caching Malloc。

1. Golang 内存管理

1.1 准备知识

这里先简单介绍一下 Golang 运行调度。在 Golang 里面有三个基本的概念:G, M, P。

G: Goroutine 执行的上下文环境。
M: 操作系统线程。
P: Processer。进程调度的关键,调度器,也可以认为约等于 CPU。
一个 Goroutine 的运行需要 G + P + M 三部分结合起来。好,先简单介绍到这里,更详细的放在后面的文章里面来说。

1.2. 逃逸分析(escape analysis)

对于手动管理内存的语言,比如 C/C++,我们使用 malloc 或者 new 申请的变量会被分配到堆上。但是 Golang 并不是这样,虽然 Golang 语言里面也有 new。Golang 编译器决定变量应该分配到什么地方时会进行逃逸分析。下面是一个简单的例子。

package main

import ()

func foo() *int {
    var x int
    return &x
}

func bar() int {
    x := new(int)
    *x = 1
    return *x
}

func main() {}

将上面文件保存为 escape.go,执行下面命令

$ go run -gcflags '-m -l' escape.go
./escape.go:6: moved to heap: x
./escape.go:7: &x escape to heap
./escape.go:11: bar new(int) does not escape

上面的意思是 foo() 中的 x 最后在堆上分配,而 bar() 中的 x 最后分配在了栈上。在官网 (golang.org) FAQ 上有一个关于变量分配的问题如下:

How do I know whether a variable is allocated on the heap or the stack?
From a correctness standpoint, you don’t need to know. Each variable in Go exists as long as there are references to it. The storage location chosen by the implementation is irrelevant to the semantics of the language.

The storage location does have an effect on writing efficient programs. When possible, the Go compilers will allocate variables that are local to a function in that function’s stack frame. However, if the compiler cannot prove that the variable is not referenced after the function returns, then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. Also, if a local variable is very large, it might make more sense to store it on the heap rather than the stack.

In the current compilers, if a variable has its address taken, that variable is a candidate for allocation on the heap. However, a basic escape analysis recognizes some cases when such variables will not live past the return from the function and can reside on the stack.

简单翻译一下。

如何得知变量是分配在栈(stack)上还是堆(heap)上?
准确地说,你并不需要知道。Golang 中的变量只要被引用就一直会存活,存储在堆上还是栈上由内部实现决定而和具体的语法没有关系。

知道变量的存储位置确实和效率编程有关系。如果可能,Golang 编译器会将函数的局部变量分配到函数栈帧(stack frame)上。然而,如果编译器不能确保变量在函数 return 之后不再被引用,编译器就会将变量分配到堆上。而且,如果一个局部变量非常大,那么它也应该被分配到堆上而不是栈上。

当前情况下,如果一个变量被取地址,那么它就有可能被分配到堆上。然而,还要对这些变量做逃逸分析,如果函数 return 之后,变量不再被引用,则将其分配到栈上。

2. 关键数据结构

几个关键的地方:

  • mcache: per-P cache,可以认为是 local cache。
  • mcentral: 全局 cache,mcache 不够用的时候向 mcentral 申请。
  • mheap: 当 mcentral 也不够用的时候,通过 mheap 向操作系统申请。
    可以将其看成多级内存分配器。

2.1 mcache

我们知道每个 Gorontine 的运行都是绑定到一个 P 上面,mcache 是每个 P 的 cache。这么做的好处是分配内存时不需要加锁。mcache 结构如下。

// Per-thread (in Go, per-P) cache for small objects.
// No locking needed because it is per-thread (per-P).
type mcache struct {
    // The following members are accessed on every malloc,
    // so they are grouped here for better caching.
    next_sample int32   // trigger heap sample after allocating this many bytes
    local_scan  uintptr // bytes of scannable heap allocated

    // 小对象分配器,小于 16 byte 的小对象都会通过 tiny 来分配。
    tiny             uintptr
    tinyoffset       uintptr
    local_tinyallocs uintptr // number of tiny allocs not counted in other stats

    // The rest is not accessed on every malloc.
    alloc [_NumSizeClasses]*mspan // spans to allocate from

    stackcache [_NumStackOrders]stackfreelist

    // Local allocator stats, flushed during GC.
    local_nlookup    uintptr                  // number of pointer lookups
    local_largefree  uintptr                  // bytes freed for large objects (>maxsmallsize)
    local_nlargefree uintptr                  // number of frees for large objects (>maxsmallsize)
    local_nsmallfree [_NumSizeClasses]uintptr // number of frees for small objects (<=maxsmallsize)
}

我们可以暂时只关注alloc [_NumSizeClasses]*mspan,这是一个大小为 67 的指针(指针指向 mspan )数组(_NumSizeClasses = 67),每个数组元素用来包含特定大小的块。当要分配内存大小时,为 object 在 alloc 数组中选择合适的元素来分配。67 种块大小为 0,8 byte, 16 byte, …,这个和 tcmalloc 稍有区别。

//file: sizeclasses.go
var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}

这里仔细想有个小问题,上面的 alloc 类似内存池的 freelist 数组或者链表,正常实现每个数组元素是一个链表,链表由特定大小的块串起来。但是这里统一使用了 mspan 结构,那么只有一种可能,就是 mspan 中记录了需要分配的块大小。我们来看一下 mspan 的结构。

2.2 mspan

span 在 tcmalloc 中作为一种管理内存的基本单位而存在。Golang 的 mspan 的结构如下,省略了部分内容。

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 // For debugging. TODO: Remove.

    startAddr     uintptr   // address of first byte of span aka s.base()
    npages        uintptr   // number of pages in span
    stackfreelist gclinkptr // list of free stacks, avoids overloading freelist
    // freeindex is the slot index between 0 and nelems at which to begin scanning
    // for the next free object in this span.
    freeindex uintptr
    // TODO: Look up nelems from sizeclass and remove this field if it
    // helps performance.
    nelems uintptr // number of object in the span.
    ...
    // 用位图来管理可用的 free object,1 表示可用
    allocCache uint64

    ...
    sizeclass   uint8      // size class
    ...
    elemsize    uintptr    // computed from sizeclass or from npages
    ...
}

从上面的结构可以看出:

next, prev: 指针域,因为 mspan 一般都是以链表形式使用。
npages: mspan 的大小为 page 大小的整数倍。
sizeclass: 0 ~ _NumSizeClasses 之间的一个值,这个解释了我们的疑问。比如,sizeclass = 3,那么这个 mspan 被分割成 32 byte 的块。
elemsize: 通过 sizeclass 或者 npages 可以计算出来。比如 sizeclass = 3, elemsize = 32 byte。对于大于 32Kb 的内存分配,都是分配整数页,elemsize = page_size * npages。
nelems: span 中包块的总数目。
freeindex: 0 ~ nelemes-1,表示分配到第几个块。

2.3 mcentral

上面说到当 mcache 不够用的时候,会从 mcentral 申请。那我们下面就来介绍一下 mcentral。

type mcentral struct {
    lock      mutex
    sizeclass int32
    nonempty  mSpanList // list of spans with a free object, ie a nonempty free list
    empty     mSpanList // list of spans with no free objects (or cached in an mcache)
}

type mSpanList struct {
    first *mspan
    last  *mspan
}

mcentral 分析:

sizeclass: 也有成员 sizeclass,那么 mcentral 是不是也有 67 个呢?是的。
lock: 因为会有多个 P 过来竞争。
nonempty: mspan 的双向链表,当前 mcentral 中可用的 mspan list。
empty: 已经被使用的,可以认为是一种对所有 mspan 的 track。
问题来了,mcentral 存在于什么地方?虽然在上面我们将 mcentral 和 mheap 作为两个部分来讲,但是作为全局的结构,这两部分是可以定义在一起的。实际上也是这样,mcentral 包含在 mheap 中。

2.4 mheap

Golang 中的 mheap 结构定义如下。

mheap_ 是一个全局变量,会在系统初始化的时候初始化(在函数 mallocinit() 中)。我们先看一下 mheap 具体结构。

allspans []*mspan: 所有的 spans 都是通过 mheap_ 申请,所有申请过的 mspan 都会记录在 allspans。结构体中的 lock 就是用来保证并发安全的。注释中有关于 STW 的说明,这个之后会在 Golang 的 GC 文章中细说。

central [_NumSizeClasses]…: 这个就是之前介绍的 mcentral ,每种大小的块对应一个 mcentral。mcentral 上面介绍过了。pad 可以认为是一个字节填充,为了避免伪共享(false sharing)问题的。False Sharing 可以参考 False Sharing - wikipedia,这里就不细说了。

sweepgen, sweepdone: GC 相关。(Golang 的 GC 策略是 Mark & Sweep, 这里是用来表示 sweep 的,这里就不再深入了。)

free [_MaxMHeapList]mSpanList: 这是一个 SpanList 数组,每个 SpanList 里面的 mspan 由 1 ~ 127 (_MaxMHeapList - 1) 个 page 组成。比如 free[3] 是由包含 3 个 page 的 mspan 组成的链表。free 表示的是 free list,也就是未分配的。对应的还有 busy list。

freelarge mSpanList: mspan 组成的链表,每个元素(也就是 mspan)的 page 个数大于 127。对应的还有 busylarge。

spans []*mspan: 记录 arena 区域页号(page number)和 mspan 的映射关系。

最新幸运 飞艇56码规律公式实战计划走势分析资金分配技巧
最新幸运 飞艇78码滚雪球规律公式实战计划分析资金分配技巧
最新幸运 飞艇78码滚雪球走势技巧与盈利规律公式计划技巧
最新幸运 飞艇56码倍投走势技巧与盈利规律公式实战计划技巧
最新《幸运 飞艇78码滚雪球规律公式》如何把握盈利走势技巧
最新《幸运 飞艇56码倍投规律公式》如何把握盈利走势技巧
最新《幸运 飞艇56码走势实战计划分析》稳赢分配资金技巧
最新《幸运 飞艇78码滚雪球走势实战计划分析》稳赢资金技巧
最新《幸运 飞艇一些78码滚雪球必中玩法》实用走势技巧规律实战经验分享
北京 赛车与幸运 飞艇七八码滚雪球公式规律盈利技巧走势分析计划
北京 赛车与幸运 飞艇七八码滚雪球规律公式如何盈利计划分析技巧
北京 赛车与幸运 飞艇七八码滚雪球如何盈利计划走势技巧分析
北京 赛车与幸运 飞艇七八码公式滚雪球分析盈利计划走势玩法
北京 赛车与幸运 飞艇七八码滚雪球规律技巧稳赢公式计划
北京 赛车PK10幸运 飞艇七八码滚雪球公式稳赢计划走势分析技巧
北京 赛车PK10幸运 飞艇七八码滚雪球稳赢技巧实战规律分析计划
北京 赛车PK10幸运 飞艇七八码滚雪球如何稳赢计划技巧走势分析
北京 赛车幸运 飞艇五六码倍投公式盈利过程计划解说走势技巧
北京 赛车幸运 飞艇五六码倍投如何盈利计划实战技巧走势分析
北京 赛车幸运 飞艇五码六码走势规律稳赚技巧实战公式计划


细分飞 艇赛 车一分钟快 三如何快速稳定赚钱
细分飞 艇赛 车一分钟快 三高手稳赢秘籍
细分飞 艇赛 车一分钟快 三赢了30万稳赢秘籍
细分飞 艇赛 车一分钟快 三绝招稳赢秘籍
细分飞 艇赛 车一分钟快 三技巧稳赢秘籍
细分飞 艇赛 车一分钟快 三杀号稳赢秘籍
细分飞 艇赛 车一分钟快 三选号稳赢秘籍
细分飞 艇赛 车一分钟快 三遗漏稳赢秘籍
细分飞 艇赛 车一分钟快 三和值走势图稳赢秘籍
细分飞 艇赛 车一分钟快 三基本走势图稳赢秘籍
细分飞 艇赛 车一分钟快 三人工计划稳赢秘籍
细分飞 艇赛 车一分钟快 三软件计划稳赢秘籍
细分飞 艇赛 车一分钟快 三稳赢秘籍


独家解读《出几买几定位胆公式》效果收益极好的玩法技巧
大神揭秘《定位胆三把必中法》玩法分享给大家
高手揭秘《定位胆怎么买准确率高》分享给大家一起交流
独家解读《11选5任2神号期期必中》效果收益极好的技巧
独家讲解分析《组三组六 必中技巧》助你快速掌握
浅析最新五星组选60玩法介绍的做号方案
浅析最新四星稳定做号思路方法的做号方案,
讲解分析最稳定的《11选5任3必中计算方法》值得收藏
独家解读《追组六不亏方法》帮你度过难关
高手全面讲解《五星组60有什么规律》命中率极高
简单实用的《五星组选30怎么算中》值得借鉴
必备攻略之《后三大底稳定700刷不停》值得收藏
最强攻略分享《后三直选单式稳赚》效果极佳
最强攻略分享《后三直选单式稳赚》效果极佳
大师深度解析《后三组六复试杀号技巧》帮助提高命中
命中最高的《后三直选单式500注万能码》分享制胜玩法
重点考虑《后三直选单式600注》需要注意的细节
高手选取《四星6000注做号思路》一定不要盲目跟进
重点考虑《四星稳定做号思路方法》需要注意的细节
讲解分析最稳定的《后三直选缩水大底》值得收藏
高手全面讲解《后三单式5胆码做号》命中率极高
玩家总结《后三组六8码杀号公式》分析取胜窍门!-
独家解读《后三直选杀号最新公式》帮你度过难关
独家解读《后三直选杀号最新公式》帮你度过难关
最值得收藏的《五星二码什么意思》帮助提高胜率
最值得收藏的《五星二码什么意思》帮助提高胜率
重点考虑《四星稳定做号思路方法》需要注意的细节
讲解分析最稳定的《后三直选缩水大底》值得收藏
高手全面讲解《后三单式5胆码做号》命中率极高
玩家总结《后三组六8码杀号公式》分析取胜窍门!-
独家解读《后三直选杀号最新公式》帮你度过难关
独家解读《后三直选杀号最新公式》帮你度过难关
最值得收藏的《五星二码什么意思》帮助提高胜率
最值得收藏的《五星二码什么意思》帮助提高胜率
独家讲解分析《五星组60和120对打》助你快速掌握
独家讲解分析《五星组60和120对打》助你快速掌握
高手全面讲解《五星组60组120如何判断》命中率极高
精准分析《怎么判断五星组选60》不可错过的细节
精准分析《怎么判断五星组选60》不可错过的细节
精准分析《怎么判断五星组选60》不可错过的细节
精准分析《怎么判断五星组选60》不可错过的细节
资深玩家解读《后三直选单式600注》玩法和技巧
资深玩家解读《后三直选单式600注》玩法和技巧
高手全面讲解《猜大小单双有什么诀窍》命中率极高
独家讲解分析《后三杀2个条件650注》助你快速掌握-
高手讲解《后三组六是什么意思》易上手玩法技巧
最新评分最高的《11选5任二怎么盈利计划》攻略分享
高手全面讲解《五星不定位胆二码秘诀》命中率极高

arena_start, arena_end, arena_used: 要解释这几个变量之前要解释一下 arena。arena 是 Golang 中用于分配内存的连续虚拟地址区域。由 mheap 管理,堆上申请的所有内存都来自 arena。那么如何标志内存可用呢?操作系统的常见做法用两种:一种是用链表将所有的可用内存都串起来;另一种是使用位图来标志内存块是否可用。结合上面一条 spans,内存的布局是下面这样的。

+-----------------------+---------------------+-----------------------+
| spans | bitmap | arena |
+-----------------------+---------------------+-----------------------+
spanalloc, cachealloc fixalloc: fixalloc 是 free-list,用来分配特定大小的块。

剩下的是一些统计信息和 GC 相关的信息,这里暂且按住不表,以后专门拿出来说。

3. 初始化

在系统初始化阶段,上面介绍的几个结构会被进行初始化,我们直接看一下初始化代码:mallocinit()。

12-12 22:21
查看更多