Go 内存管理 | 青训营

48 阅读3分钟

堆内存结构

mspan

mspan 是 Go 中内存管理的基本单元,一组连续的 page 组成 1 个 mspan,每个 page 大小为 8 KB

mcache

mcache 存储在每个工作线程的栈上,其中包含了一组空闲的内存块,这些块可以用来分配新的对象。当一个 goroutine 需要分配内存时,它首先会检查自己的 mcache 是否有足够大小的空闲块,如果有,则直接从 mcache 中分配;如果没有,则会从全局内存池中获取新的内存 span,并将其中的一部分分配给 mcache,以供该线程后续使用。每个工作线程(goroutine)都拥有自己的 mcache,这有助于避免多个线程之间的争夺和竞争,提高了内存分配的性能

mcentral

mcentral 是全局内存管理的一部分,它管理这些工作线程特定的 mcache,并在需要时分配给它们。当一个 goroutine 需要分配内存时,它首先会检查自己的 mcache 是否有足够大小的空闲块,如果有,则直接从 mcache 中分配;如果没有,就会从关联的 mcentral 中请求更多的内存块。mcentral 维护了一些列不同大小的 mspan 列表,每个列表对应一种特定大小的对象。当一个 mcache 需要更多内存时,它会向 mcentral 请求合适大小的内存块,然后将这些内存块分配给 mcache,供工作线程后续使用

mheap

mheap 作为 Go 语言内存分配器的一部分,负责维护整个堆的状态,并确保有效地分配和释放内存

内存逃逸

内存逃逸(Memory Escape)是指在编译时,由于编译器无法确定变量的生命周期或者变量需要在函数外部继续存在,从而导致编译器将变量分配在堆内存上而不是栈内存上的情况。这种情况会引起额外的内存分配和垃圾回收负担,可能对程序性能产生影响。内存逃逸会在以下情况下发生:

  1. 函数返回引用类型。当一个函数返回一个引用类型(如指针、slice、map、接口等)并且该引用类型在函数外被使用时,编译器无法确定变量的生命周期,从而将其分配在堆上,以确保其在函数外部仍然有效
  2. 闭包引用。当一个闭包(匿名函数)引用了函数内的局部变量时,闭包可能会在函数外被执行,这就导致了内存逃逸。编译器会将闭包引用的变量分配在堆上,以保证闭包执行时变量仍然有效
  3. 接口类型的逃逸。当一个变量被赋值给接口类型,并且该接口变量逃逸到函数外部,编译器会将变量分配在堆上。这是因为接口类型可能包含任何类型的值,编译器无法在编译时确定具体类型和大小
  4. 动态调用。使用反射等技术进行动态调用时,编译器无法在编译时确定具体的调用逻辑和类型,因此可能会导致内存逃逸
  5. 切片预分配不足。当创建一个切片并在后续操作中追加元素,如果切片的容量不足,会触发切片的重新分配,可能导致内存逃逸
  6. 变量逃逸到多个 goroutine。如果一个变量逃逸到多个并发的 goroutine 中,编译器无法确定它在哪个 goroutine 中被访问,可能会导致内存逃逸

避免不必要的内存逃逸的方法:

  • 尽量将局部变量的作用域限制在函数内部
  • 避免将局部变量传递给返回的闭包
  • 在可能的情况下,使用值类型而不是引用类型
  • 预分配切片容量以避免重新分配
  • 避免不必要的接口使用和类型断言
  • 了解编译器的优化行为,进行合理的代码设计