目录

前言:        

1. GMP调度模型

2. Go的内存管理

3. GC垃圾回收机制

4. 总结


前言:        

        Go语言在处理并发问题上相比较其他语言具有优势,这背后离不开go runtime的运行机制。Go 并没有绕开操作系统,代码程序最终仍要借助操作系统线程在CPU上执行,内存也仍然来自操作系统。Go是在语言代码与操作系统之间增加一层runtime,由它统一管理并发任务和内存生命周期。本文针对Go 运行时如何管理 goroutine和内存为主线,给大家讲解一下go语言的GMP 调度模型与 GC 垃圾回收等核心机制。

1. GMP调度模型

GMP 是 Go 程序运行时的一个关键机制,用于管理内存和调度协程在多个线程之间执行。

  • G(Goroutine):G就是groutine的意思,代表一个协程,每次调用的时候都会创建一个G对象,它包含栈、指令指针和对于调用groutine的状态参数等重要信息

  • M(Machine):M代表了一个线程,每次创建一个M的时候,都会有一个底层操作系统线程创建,所有的G任务,最终还是依附于M上执行

  • P(Processor):执行器,相当于一个管理者。每一个M必须绑定一个P。P负责调度groutine,自身有一个局部本地队列用来保存groutine,并且所有的P都共享着一个全局队列。M从P上获得groutine并执行,同时还负责部分内存管理。

只有拿到 P 的 M,才能执行用户 Go 代码——这是理解整个调度模型的关键前提。

执行顺序为:

1. M向P要G执行时,若是P自己的本地队列有G那么直接拿G来执行
2. 若是本地队列没有G,那么就去全局队列里面去拿
3. 若是全局队列也没有G,那么就会去其他的MP组合去抢夺G(Work Stealing机制)

当某个 P 的本地队列堆满、另一个 P 却空闲时,空闲的 M 会主动去偷一半任务过来,这就是 Work Stealing机制,避免出现一边排队、一边闲置的情况。而且如果当前M因为channel发送或者系统调用阻塞,那么P会与M解绑,并将其转移给其他空闲M与之绑定(hand off 机制),或者新建一个M执行G。

2. Go的内存管理

上文 G 被调度到 M 上开始执行后,首先要面对两个基本的内存区域:

  • :每个 goroutine 都有自己的用户栈,函数调用形成栈帧,参数和局部变量大多存在这里,函数返回后栈帧空间可以直接被复用。

  • :用来存放生命周期无法绑定到某个函数栈帧的对象——如果函数返回后对象仍可能被引用,它就必须活在栈帧之外的地方,而堆对象什么时候可以释放,只能靠 GC 判断来决定。

有一种简化说法是栈不需要 GC,但更准确的理解是:栈上的对象不需要像堆对象那样逐个标记、清扫,但 goroutine 栈本身是重要的 GC Root,栈里指向堆的指针必须被扫描到——否则 GC 会把仍在使用的对象误判成垃圾。

逃逸分析

一个变量最终应该放在栈还是堆上,是由编译器根据使用方式判断,逃逸分析就是用来判断一个变量应该被分配到栈上还是堆上。如果一个变量的生命周期是完全可知的, 并且没有被函数外部引用, 那么它就可以分配到栈上,否则就会发生逃逸,必须分配在堆上。

// newNumber 返回局部变量地址用于演示逃逸
func newNumber() *int {
    x := 10
    return &x
}

x 的地址通过返回值离开了newNumber 的栈帧。函数结束后,调用方仍可能通过指针访问它,所以编译器必须保证 x 继续存活。在没有被内联等优化改变上下文时,x 通常会逃逸到堆。

3. GC垃圾回收机制

为什么需要垃圾回收:

在 C 语言中,开发者需要通过 malloc和free 手动管理内存。手动管理虽然能够获得较高的控制能力,但也容易引入内存泄漏、悬空指针、重复释放等问题。
Go 选择将堆内存管理交给 Runtime,通过垃圾回收器(GC)自动回收不再使用的对象。GC 判断对象是否回收,并不是根据业务逻辑判断。例如,一个订单对象、缓存数据或者连接对象是否已经没有业务价值,GC 无法判断感知。GC 只能通过可达性分析判断对象是否仍然被程序引用。

回收流程:

Go语言使用三色标记法作为垃圾回收算法之一,回收过程主要是分标记和清除两个阶段。

三色标记使用白、灰、黑三种逻辑颜色表示扫描进度:

  • 白色:当前周期尚未发现的对象,标记结束后仍为白色才会被视为不可达

  • 灰色:对象已经被发现,但它内部保存的指针还没有扫描完成

  • 黑色:对象已经完成扫描,它指向的已发现对象也已进入标记流程

标记原理:

1. 所有对象初始都标记为白色,表示这些对象尚未被扫描。
2. 从根对象(比如全局变量,栈中变量)开始,将其引用的对象标记为灰色,表示这些对象已经被扫描过,但其引用的对象还未被扫描。
3. 继续对灰色对象进行扫描,将其引用的对象标记为灰色, 并将当前灰色对象标记为黑色,表示这些对象已经被扫描过,其引用的对象也已经被扫描过。
4. 遍历所有白色对象,将其标记位死亡对象,进行垃圾回收。

白色不等于此刻就是垃圾。标记尚未结束时,一个白色对象可能只是还没有被扫描路径发现。只有整个标记完成后仍然不可达的白色对象,才会进入清扫范围。

由于标记操作和用户程序是并发执行的,用户逻辑会时常生成对象或者改变对象的引用导致状态不一致,比如一个对象标记为白色准备回收时,用户又引用或者创建新对象。为了解决这个问题go1.8 引入写屏障机制。
写屏障就是一种运行时机制,当程序修改对象引用关系时,会记录这次变化,通知GC重新处理相关对象,保证存活对象能够被正确标记,避免被GC错误回收。

GC执行流程:

GC触发

↓

标记阶段
(三色标记 + 写屏障)

↓

Mark Termination
(短暂STW,完成最终标记)

↓

清除阶段
(清扫不可达对象)

↓

内存复用

清除阶段:

标记完成后:

  • 黑色对象:保留
  • 白色对象:不可达,进行回收

Go GC中的标记和清扫阶段均可以与用户程序并发执行,仅在标记开始和结束阶段存在短暂STW。

4. 总结

        GMP 解决的是任务如何获得执行机会——G 是协程、M 是执行代码的线程、P 是调度者与本地资源的载体;
        内存管理解决的是对象该放在哪儿——栈成本低但生命周期受限,逃逸到堆的对象则交给GC管理;
        GC 解决的是堆如何持续可用——通过三色标记判断可达性,借助写屏障保证并发标记的正确性,用短暂 STW 和并发清扫在暂停时间、CPU 与内存之间取得平衡。
        而串起这一切的关键,是调度器提供的安全点和执行资源——GMP让程序跑起来,GC让内存能一直用下去。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐