day7 Go 内存分配(1) | 青训营笔记

109 阅读4分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 7 天.

一. 前言

1. 一般程序内存布局:

一般程序分为5部分:栈区(stack),堆区(heap),静态/全局区,文字常量区,代码区

  • 栈区 : 由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈.(C语言中的const 局部变量也存储在栈区),栈区向地址减小的方向增长.
  • 堆区 : 一般由程序员申请和释放(如C/C++,程序员调用malloc()函数申请,free()函数释放),也有由编译器分配释放的(如Go),与数据结构中的堆没有任何关系,分配方式类似于链表. 系统会寻找大于或者等于所申请空间大小的内存分配,如果大于,则会将多余内存分开,容易造成内存碎片化.
  • 静态/全局区 : 全局变量和静态变量的存储是放在一块的,被分配后一直到程序结束才会由释放.
  • 常量区 : 存放常量的区域,常量是指在程序运行过程中不能被改变的量.
  • 代码区 : 存储运行程序代码(CPU执行的机械指令)的区域,一般是只读的. image.png
func main() {
   z := escape
   a, b := escape()
   x, y := 10, 11
   fmt.Printf(" x : %v \n y : %v \n heap :\n func : %v \n a : %v \n b : %v \n", &x, &y, &a, &b, &z)
}

func escape() (*int, *int) {
   x := 10
   y := 11
   z := 12
   fmt.Printf(" stack :\n z : %v\n", &z)
   return &x, &y
}

image.png

  • 逃逸出的变量被分配在堆上
  • 分配在堆的变量比分配在栈变量的变量,内存地址低

2. Go语言内存分配

Go语言的runtime,抛弃了传统的内存分配方式,改为自主管理,Golang运行时的内存分配算法借鉴了 Google 为 C 语言开发的TCMalloc算法.
TCMalloc核心思想是多级管理,从而降低锁的粒度.go语言学习了这点,每个线程都会自行维护一个独立的内存池,进行内存分配时优先从该内存池中分配,当内存池不足时才会向全局内存池申请,以避免不同线程对全局内存池的频繁竞争.

二.Go语言如何处理在堆还是栈上的分配?

1.Go语言分配规则

  • escape analyze(能够自动分析出变量的作用范围,是否将变量分配堆上)
  • Go编译器会尽可能将变量分配到到栈上。但是,当编译器无法证明函数返回后,该变量没有被引用,那么编译器就必须在堆上分配该变量,以此避免悬挂指针(dangling pointer)。另外,如果局部变量非常大,也会将其分配在堆上。

2.GO分配的结构:

Go在程序启动的时候,会先向操作系统申请一块内存(注意这时还只是一段虚拟的地址空间,并不会真正地分配内存),切成小块后自己进行管理。
申请到的内存块被分配了三个区域,在X64上分别是512MB,16GB,512GB大小。 image.png

  • area : 堆区,GO动态分配就在这个区域,它把内存分割为8Kb大小的页,一些页组合起来就是mspan.
  • bitmap : 标识arena区域哪些地址保存了对象,并且用4bit标识位标识对象是否包含指针,GC标记信息.bitmap中一个byte大小对应arena中的4个指针大小(4 * 8 B)的内存,所以bitmap的大小为512 GB / (4 * 8 B) = 16GB.bitmap的高地址部分指向arena的低地址部分.
  • spans : 存放span的指针,每个指针对应一页,spans的大小为512GB / 8KB * 8B = 512MB.除以8KB是计算arena的页数,而最后乘以8是计算spans所有指针的大小。

2.mspan

  • mspan : Go中内存管理的基本单元,是由一片连续的8KB的页组成的大块内存。mspan是一个包含起始地址、mspan规格、页的数量等内容的双端链表。
  • mspan按照它自身的属性class_to_size的大小分割成若干个object,每个object可存储一个对象。
  • mspan结构 :
// path: /usr/local/go/src/runtime/mheap.go

type mspan struct {
    //链表前向指针,用于将span链接起来
    next *mspan 
    //链表前向指针,用于将span链接起来
    prev *mspan 
    // 起始地址,也即所管理页的地址
    startAddr uintptr 
    // 管理的页数
    npages uintptr 
    // 块个数,表示有多少个块可供分配
    nelems uintptr 
    //分配位图,每一位代表一个块是否已分配
    allocBits *gcBits 
    // 已分配块的个数
    allocCount uint16 
    // class表中的class ID,和Size Classs相关
    spanclass spanClass  
    // class表中的对象大小,也即块大小
    elemsize uintptr 
}
  • class_to_size : 内存块的大小,共有67种,被写死在go标准库:runtime/sizeclasses.go.
  • 每个 class_to_size有两个mspan,也就是有两个spanclass。其中一个分配给含有指针的对象,另一个分配给不含有指针的对象。

参考资料

zhuanlan.zhihu.com/p/59125443
studygolang.com/articles/01…