Go 2025-03-28 · 15 min 阅读

Go内存分配与GC

Go语言GMP介绍 Go语言相比Java,有更好的并发能力(GMP模型),同时其占用的服务器资源也较少,了解一下GMP的理念。从操作系统层面来看,线程是指内核级线程,是操作系统最小调度单元,创建、销毁、调度交由内核完成,可充分利用多核。协程(用户线程)与线程存在M:1的映射关系,从属于同一个内存级线程,无法并行,并且,一个协程阻塞会导致从属同一线程的所有协程无法执行。 Goroutine 经Golang优化后的协程,其有如下特点:1)与线程存在映射关系,为M:N;2)创建、销毁、调度在用户态完成,对内核透明,足够轻便;3)可利用多个线程,实现并行;4)通过调度器的斡旋,实现和线程间的动态绑定和灵活调度;5)栈空间大小可动态扩缩,因地制宜; 在/runtime/proc.go的代码注释中,有对GMP的解释,其核心数据结构在/runtime/runtime2.go:


// Goroutine scheduler, Design doc at https://golang.org/s/go11sched.
// The scheduler's job is to distribute ready-to-run goroutines over worker threads.
//
// The main concepts are:
// G - goroutine.
// M - worker thread, or machine.
// P - processor, a resource that is required to execute Go code.
//     M must have an associated P to execute Go code, however it can be
//     blocked or in a syscall w/o an associated P.

gmp模型其要点和调度规则如下:

GMP调度

g0是一种特殊的调度协程,不执行用户函数,负责执行g之间的切换调度,与m的关系为1:1。goroutine的类型可分为两类:

当g0找到可执行g时,会调用gogo方法,调度g执行用户定义的任务。当g需要主动让渡时,会触发mcall方法,将执行权限重新交给g0;

广义”调度”可分为几种类型:

值得一提的是,前3种调度方式都由m下的g0完成。而抢占调用则是由一个全局监控协程monitor g来监控,倘若发现满足抢占调度的条件,则会从第三方的角度出手干预,主动发起该动作。从宏观上:

p每执行61次,会从全局队列中获取一个goroutine进行执行,同时会额外将全局队列中的一个goroutine放到本地队列中。若本地队列已满,则会返回来将本地队列中一半的g放回全局队列中,帮助当前p缓解执行压力;

Go内存模型与分配机制

在操作系统中,存在寄存器、高速缓存、内存和磁盘,越接近cpu存储的容量越小,其对应的价格就越高昂。页表、分页管理等机制来减少内存碎片。

Golang中的内存模型,以空间换时间,一次缓存,多次复用。堆mheap正是基于该思想,产生的数据结构。依次细化粒度,建立了 mcentral、mcache 的模型,下面对三者作个梳理:

内存单元mspan

page和mspan的2个概念,page:最小的存储单元,默认大小为8KB,mspan大小为page的整数倍,且从8B到80KB 被划分为67种不同的规格,对应源代码在runtime/sizeclasses.go,mspan具有如下特点:

线程缓存mcache

中心缓存mcentral

要点:

全局堆缓存mheap

对象分配流程

不论是以下哪种方式,最终都会殊途同归步入mallocgc方法中,例如:new(T)、&T{}、make(xxxx),Golang中,依据object的大小,会将其分为下述三类:tiny微对象(0, 16B)、small小对象(16B,32KB)、large大对象(32KB,正无穷). 对于微对象的分配流程:

  1. 从P专属mcache的tiny分配器取内存(无锁)
  2. 根据所属的spanClass,从P专属mcache缓存的mspan中取内存(无锁)
  3. 根据所属的spanClass从对应的mcentral中取mspan填充到mcache,然后从mspan中取内存(spanClass粒度锁);
  4. 根据所属的spanClass,从mheap的页分配器pageAlloc取得足够数量空闲页组装成mspan填充到mcache,然后从mspan中取内存(全局锁);
  5. mheap`向操作系统申请内存,更新页分配器的索引信息,然后重复(4);

对于小对象的分配流程是跳过(1)步,执行上述流程的(2)-(5)步; 对于大对象的分配流程是跳过(1)-(3)步,执行上述流程的(4)-(5)步.

Go垃圾回收原理

做Java的都对GC比较熟悉,在JVM中常见的GC算法有:标记整理(Mark-Sweep)、标记压缩(Mark-Compact)、半空间复制(类似于G1),通过引用计数寻找不可达对象,便于垃圾回收。

Go中三色标记法

Golang GC中用到的三色标记法属于标记清扫-算法下的一种实现,由荷兰的计算机科学家Dijkstra提出,下面阐述要点:

为了应对并发情况下,对象标记出现漏标、多标的情况,可使用屏障机制。漏标问题的本质就是,一个已经扫描完成的黑对象指向了一个被灰\白对象删除引用的白色对象. 一套用于解决漏标问题的方法论称之为强弱三色不变式:

# Go