Go语言内存管理 | 青训营笔记

80 阅读4分钟

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

来自前段时间读Go语言专家编程的读书笔记

内存分配管理

申请的内存会被划分成三个部分

  • arena:即堆区,应用需要的内存也是从这里分配出去的,这个区域会划分成 page页,每个页8KB
  • spans:存放span指针的区域,每个指针又对应一个或多个page
  • bitmap:和GC标志有关

image.png

span:用来管理arena中page页的,包含一个或多个连续页,页又会根据需要划分出更小的粒度class,每个span管理特定的大小的class对象`

type mspan struct {
	next *mspan     // 链表前向指针
	prev *mspan     // 链表后向指针
	list *mSpanList // For debugging. TODO: Remove.

	startAddr uintptr // 管理的页开始地址
	npages    uintptr // 页数

	manualFreeList gclinkptr // list of free objects in mSpanManual spans
  nelems uintptr // 块个数
  allocBits  *gcBits		// 每一位代表一个块是否被分配
	gcmarkBits *gcBits		// 每块的gc标记情况
	allocCache uint64
  sweepgen              uint32
	allocCount            uint16        // 已被分配的块数
	spanclass             spanClass     // size class and noscan (uint8)
	state                 mSpanStateBox // 
	needzero              uint8         // needs to be zeroed before allocation
	allocCountBeforeCache uint16        // 
	elemsize              uintptr       // 块大小
	limit                 uintptr       // 
	speciallock           mutex         // guards specials list
	specials              *special      // 
}

image.png

cache:为了避免多线程申请内存时频繁加锁,为每个线程分配了span的缓存

数组中每个元素代表一种class类型的span列表,列表又有两种,一种包含了指针,一种不包含指针,目的是为了方便GC扫描,没指针对象的就没必要去扫描他

type mcache struct {
	alloc [numSpanClasses]*mspan // spans to allocate from, indexed by spanClass
	stackcache [_NumStackOrders]stackfreelist
}

image.png

central:用来管理span,这么说吧,cache是为了单个线程提供服务缓存,central则是为了多线程服务。

当某个线程内存不足时向central申请,当线程释放又回收到central,cache初始时是没有span的,过程中动态的从central获取并缓存下来

  • 申请span过程:加锁、从空闲表中取span并从链表中删除加入到非空闲链表中,span返回给线程,解锁、线程将span缓存到cache
  • 释放span过程:加锁、从非空闲链表删除加到空闲链表,解锁
type mcentral struct {
	spanclass spanClass
	partial [2]spanSet // 还有空闲块的span列表
	full    [2]spanSet // 没有空闲块的span列表
}

heap:知道了central管理的某一种大小的class类型的span,这些central又存放在 heap中,Go也就是通过一个heap管理内存

type mheap struct {
	lock mutex
  allspans []*mspan // all spans out there
  
  curArena struct {	// 当前arena区域的起始和已使用的地址
		base, end uintptr
	}
  
  central [numSpanClasses]struct { //  每种类型class对应的central
		mcentral mcentral
		pad      [cpu.CacheLinePadSize -unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize]byte
	}
}

总结就是下面这张图

image.png

垃圾回收

常见的垃圾回收算法:

  • 引用计数:每个对象维护一个计数,引用该对象的对象被销毁则减1,直到0就回收对象
    • 好处就是能够很快的发现并回收,不会等到内存耗尽才回收,缺点是不能很好处理循环引用,实时维护计数也要代价
  • 分代收集:按照对象生命周期长短划分不同的代空间,也就是老年代、新生代,不同代有不同的回收算法和频率
    • 好处就是回收性能好,但是算法复杂
  • 标记清除:从根变量开始遍历引用的对象,引用的标记为“被引用”,没有的就会被回收
    • 好处是避免了引用计数的循环引用问题,但是需要STW(停止所有协程,专心做垃圾回收)

三色标记:

  • 白色:对象未标记(gcmarkBits 对应位 0)
  • 灰色:对象在标记队列中等待
  • 黑色:对象被标记(gcmarkBits 对应位 1)

使用混合写屏障优化减弱STW image.png

逃逸分析

  • 如果分配在栈中,函数执行结束自动内存回收
  • 如果分配在堆中,函数执行结束可交给GC处理

编译器根据对象是否被函数外部引用来决定是否逃逸:

  • 没被引用,优先放入栈中
  • 被引用,一定是放到堆中

还有当内存过大超过栈存储能力时,也会放到堆中

  • 动态类型逃逸
func main(){
  s := "test"
  fmt.Println(s)
  // Println(a...interface{}),编译期间很难确定参数具体类型,也会产生逃逸
}
  • 闭包引用
func fibonaci() func() int {
  a,b := 0,0
  return func() int {
    a,b = b,a+b
    return a
  }
  // 闭包引用了局部变量a、b,所以被放入到堆中
}

Go编译器优化

函数内联

  • 定义:将被调用函数的函数体的副本替换到调用位置上,同时重写代码以反映参数的绑定

  • 优点 - 消除调用开销 - 将过程间分析的问题转换为过程内分析,帮助其他分析

  • 缺点 - 函数体变大 - 编译生成的 Go 镜像文件变大

  • 采取一定的策略决定是否内联 - 调用和被调用函数的规模

  • Go 内联的限制 - 语言特性:interface, defer 等等,限制了内联优化 - 内联策略非常保守

  • 开销 - Go 镜像大小略有增加 - 编译时间增加 - 运行时栈扩展开销增加