跳转至

2 memory

2 内存分配原理

  • 堆栈 & 逃逸分析
  • 持续栈
  • 内存管理

2.1 堆栈 & 逃逸分析

2.1.1 堆栈定义

Go 分配内存的方式有两个:

  • 全局堆空间用来动态分配内存;
  • 每个 Goroutine 都有一个自身的栈空间(2KB);

  • 栈区的内存一般由编译器自动进行分配与释放,其中存储着函数的入参以及局部变量,这些参数会随着函数的创建而创建,函数的返回而销毁(通过 CPU push & release);

  • 堆区的内存一般由编译器和工程师自己共同进行管理分配,交给 Runtime GC 来释放。堆上分配必须找到一块足够大的内存来存放新的变量数据。后续释放是,垃圾回收器扫描堆空间寻找不再被使用的对象;
  • 栈分配廉价,堆分配昂贵;

2.1.2 逃逸分析

“通过检查变量的作用域是否超出了它所在的栈来决定是否将它分配在堆上”的技术,其中“变量的作用域超出了它所在的栈”的这种行为称之为逃逸; 逃逸分析在大多数语言中属于静态分析在编译期由静态代码分析来决定一个值是否被分配在栈帧上,还是需要“逃逸”到堆上

  • 减少 GC 压力,栈上的变量,随着函数退出后系统直接回收,不需要标记后在清除;
  • 减少内存碎片的产生;
  • 减轻分配堆内存的开销,提高程序的运行速度。
func main(){
    num := getRandum()
    println(*num)
}

func getRandum() *int{
    tmp := rand.Intn(10)

    return &tmp
}

查看逃逸分析的命令:

go build -gcflags '-m'

2.1.3 超过栈帧(stack frame)

超过栈帧(stack frame): 当一个函数被调用的时候,会在两个相关的帧边界间进行上下文切换。从调用函数切换到被调用函数,如果函数调用时需要传递参数,那么这些参数值也要传递到被调用函数的帧边界中。Go 语言中帧边界间的数据传递是按照值进行传递的。任何在函数 getRandom 中的变量在函数返回时,都将不能访问。Go 查找所有变量超过当前函数栈帧的,把他们分配到堆上, 避免 outlive 变量。

2.2 持续栈

Go 运行时,每个 goroutine 都维护了一个自己的栈区,这个栈区自己使用并不能被其他 goroutine 使用。栈区的初始化大小是 2KB,在 goroutine 运行的时候栈区会按照需要增长和收缩,占用的内存最大默认值的限制是在64位系统上是 1GB。

segmentted_stacks

2.2.1 Hot split 问题

分段栈的实现方式存在 hot split 问题,若是栈快满了,在下次的函数调用就会强制触发栈扩容。当函数返回时,新分配的 stack chunk 会被清理掉。如果这个函数调用产生的一个返回是在一个循环中,会导致一个严重的性能问题,会频繁的 alloc/free 。

2.2.2 连续栈 Contiguous stacks

采用复制栈的实现方式,在热分裂的场景中不会频发释放内存,既不像分配一个新的内存快并把老的内存块内容复制到新的内存快里,当栈缩减回之前的大小时,我们不需要做任何事情。

Contiguous stack

2.2.3 栈扩容 More stack

Go 运行时判断栈空间是否足够,所以在 call func 的时候会插入 runtime.morestack , 但每个函数调用都判定的话,成本比较高。在编辑期间通过计算 sp、func stack framesize 确定需要那个函数调用中插入 runtime.morestack。

2.3 内存管理

TCMalloc 是 Thread Cache Malloc 的简称,是 Go 内存管理的起源,Go 的内存管理是借鉴了 TCMalloc

  • 内存碎片 随着内存不断的申请和释放,内存上会存在大量的碎片,降低内存的使用率。为了解决内存碎片,可以将 2个或者多个连续的未使用的内存快喝冰,减少碎片;
  • 大锁 同一个进程下所有的线程共享相同的内存空间,他们申请使用内存时需要加锁,如果不加锁就存在同一块内存被 2个甚至多个线程同时访问的问题;

2.3.1 小于 32kb 内存分配

当程序里发生了 32kb 以下的小内存的申请时, Go 会从 mcache 的本地缓存给进程分配内存。对应的内存块叫 mspan ,他是要给程序分配内存时的分配单元。

在 Go 的调度模型中,每个 M 会绑定给一个 P ,在单一粒度的时间里只能做多处理运行一个 goroutine, 每个 P 都会绑定一个 mcache 。当需要进行内存分配时,当运行的 goroutine 会从 mcache 中查找可用的 mspan。从本地 mcache 里分配内存时不需要加锁,这种分配策略效率更高。

  • macache 本地缓存
  • mcentral 全局中心缓存
  • mheap 全局堆(内存页 8KB)

Reference

  • https://github.com/dgryski/go-perfbook/blob/master/performance-zh.md
  • https://cch123.github.io/perf_opt/
  • https://blog.cloudflare.com/how-stacks-are-handled-in-go/
  • https://agis.io/post/contiguous-stacks-golang/