|
对于很多需要重复分配、回收内存的地方,sync.Pool 是一个很好的选择。频繁 地分配、回收内存会给 GC 带来一定的负担,严重的时候会引起 CPU 的毛刺。 而 sync.Pool 可以将暂时将不用的对象缓存起来,待下次需要的时候直接使用, 不用再次经过内存分配,复用对象的内存,减轻 GC 的压力,提升系统的性 能。 |
Golang 在语言级别支持协程,称之为 Goroutine。Golang 标准库提供的所有系统 调用操作(包括所有的同步 I/O 操作),都会出让 CPU 给其他Goroutine。这让 Goroutine 的切换管理不依赖于系统的线程和进程,也不依赖于 CPU 的核心数 量,而是交给 Golang 的运行时统一调度。 |
G(Goroutine):我们所说的协程,为用户级的轻量级线程,每个 Goroutine 对象中的 sched 保存着其上下文信息。
象)。
P(Processor):即为G和M 的调度对象,用来调度 G和M 之间的关联关系, 其数量可通过 GOMAXPROCS()来设置,默认为核心数。
3、1.0 之前 GM 调度模型
|
损耗。为了解决这一的问题 go从1.1 版本引入,在运行时系统的时候加入 p对象, 让 P 去管理这个G 对象,M 想要运行G,必须绑定 P,才能运行 P 所管理的G。 GM 调度存在的问题: 1. 单一全局互斥锁(Sched.Lock)和集中状态存储 2. Goroutine 传递问题(M 经常在 M 之间传递”可运行”的 goroutine) 3.每 个M 做内存缓存,导致内存占用过高,数据局部性较差 4.频繁syscall 调用,导致严重的线程阻塞/解锁,加剧额外的性能损耗。 |
|
|
l 每个P 和一个M 绑定,M 是真正的执行P中goroutine 的实体(流程 3),M 从绑 定的P 中的局部队列获取 G 来执行
l 当M 绑定的P 的局部队列为空时,M 会从全局队列获取到本地队列来执行 G
(流程3.1),当从全局队列中没有获取到可执行的 G 时候,M 会从其他P的 局部队列中偷取 G 来执行(流程3.2),这种从其他 P 偷的方式称为work stealing
l 当G 因系统调用(syscall)阻塞时会阻塞 M,此时P 会和M 解绑即 hand off,并 寻找新的 idle的M,若没有 idle的M 就会新建一个M(流程5.1)
- 当G因channel 或者network I/O 阻塞时,不会阻塞 M,M 会寻找其他runnable的 G;当阻塞的 G 恢复后会重新进入 runnable 进入P 队列等待执行(流程5.3)
获取 P 本地队列,当从绑定 P 本地 runq 上找不到可执行的 g,尝试从全局链表中 拿,再拿不到从 netpoll 和事件池里拿,最后会从别的 P 里偷任务。P 此时去唤 醒一个 M。P 继续执行其它的程序。M 寻找是否有空闲的 P,如果有则将该G 对 象移动到它本身。接下来 M 执行一个调度循环(调用G 对象->执行->清理线程→ 继续找新的 Goroutine 执行)
|
当本线程 M 因为G 进行的系统调用阻塞时,线程释放绑定的 P,把P 转移给其他 空闲的 M 执行。 细节:当发生上线文切换时,需要对执行现场进行保护,以便下次被调度执行 时进行现场恢复。Go 调度器M 的栈保存在G 对象上,只需要将 M 所需要的寄存器
(SP、PC 等)保存到 G 对象上就可以实现现场保护。当这些寄存器数据被保护起 来,就随时可以做上下文切换了,在中断之前把现场保存起来。如果此时 G 任 务还没有执行完,M 可以将任务重新丢到 P 的任务队列,等待下一次被调度执 行。当再次被调度执行时,M 通过访问G的vdsoSP、vdsoPC 寄存器进行现场恢复
(从上次中断位置继续执行)。
7、协作式的抢占式调度
在1.14 版本之前,程序只能依靠 Goroutine 主动让出 CPU 资源才能触发调度。 这种方式存在问题有:
l 某些 Goroutine 可以长时间占用线程,造成其它 Goroutine 的饥饿
l 垃圾回收需要暂停整个程序(Stop-the-world,STW),最长可能需要几分 钟的时间,导致整个程序无法工作
|
- I/O,select
- block on syscall
- channel
l 等待锁
- runtime.Gosched()
10、Sysmon 有什么作用
Sysmon 也叫监控线程,变动的周期性检查,好处
l 释放闲置超过 5 分钟的 span 物理内存;
l 如果超过 2 分钟没有垃圾回收,强制执行;
l 将长时间未处理的 netpoll 添加到全局队列;
l 向长时间运行的 G 任务发出抢占调度(超过10ms的g,会进行 retake);
l 收回因 syscall 长时间阻塞的 P;
11、三色标记原理
我们首先看一张图,大概就会对 三色标记法有一个大致的了解:
|
|
原理: 首先把所有的对象都放到白色的集合中 l 从根节点开始遍历对象,遍历到的白色对象从白色集合中放到灰色集合中 l 遍历灰色集合中的对象,把灰色对象引用的白色集合的对象放入到灰色集 合中,同时把遍历过的灰色集合中的对象放到黑色的集合中 l 循环步骤 3,知道灰色集合中没有对象 |
l 步骤4 结束后,白色集合中的对象就是不可达对象,也就是垃圾,进行回收
12、写屏障
Go 在进行三色标记的时候并没有 STW,也就是说,此时的对象还是可以进行修 改。
那么我们考虑一下,下面的情况。
|
|
我们在进行三色标记中扫描灰色集合中,扫描到了对象 A,并标记了对象 A 的 所有引用,这时候,开始扫描对象 D 的引用,而此时,另一个 goroutine 修改了 D->E 的引用,变成了如下图所示 |
|
|
13、插入写屏障
Go GC 在混合写屏障之前,一直是插入写屏障,由于栈赋值没有 hook 的原 因,栈中没有启用写屏障,所以有 STW。Golang 的解决方法是:只是需要在结 束时启动 STW 来重新扫描栈。这个自然就会导致整个进程的赋值器卡顿。
14、删除写屏障
Golang 没有这一步,Golang 的内存写屏障是由插入写屏障到混合写屏障过渡 的。简单介绍一下,一个对象即使被删除了最后一个指向它的指针也依旧可以 活过这一轮,在下一轮 GC 中才被清理掉。
15、混合写屏障
|
l 混合写屏障继承了插入写屏障的优点,起始无需 STW 打快照,直接并发扫 描垃圾即可; l 混合写屏障继承了删除写屏障的优点,赋值器是黑色赋值器,GC 期间,任 何在栈上创建的新对象,均为黑色。扫描过一次就不需要扫描了,这样就 消除了插入写屏障时期最后 STW 的重新扫描栈; l 混合写屏障扫描精度继承了删除写屏障,比插入写屏障更低,随着带来的 是 GC 过程全程无 STW; l 混合写屏障扫描栈虽然没有 STW,但是扫描某一个具体的栈的时候,还 是要停止这个 goroutine 赋值器的工作(针对一个 goroutine 栈来说,是暂 停扫的,要么全灰,要么全黑哈,原子状态切换)。 |
主动触发:调用 runtime.GC 被动触发: |
使用系统监控,该触发条件由 runtime.forcegcperiod 变量控制,默认为 2 分钟。当 超过两分钟没有产生任何 GC 时,强制触发 GC。 使用步调(Pacing)算法,其核心思想是控制内存增长的比例。如 Go 的 GC 是一种比例 GC, 下一次 GC 结束时的堆大小和上一次 GC 存活堆大小成比例.
Go1.14 版本以 STW 为界限,可以将 GC 划分为五个阶段: GCMark 标记准备阶段,为并发标记做准备工作,启动写屏障 STWGCMark 扫描标记阶段,与赋值器并发执行,写屏障开启并发
|
GCMarkTermination 标记终止阶段,保证一个周期内标记任务完成,停止写屏障 GCoff 内存清扫阶段,将需要回收的内存归还到堆中,写屏障关闭GCoff 内存归还阶段,将过多的内存归还给操作系统,写屏障关闭。
18、GC 如何调优
通过 go tool pprof 和 go tool trace 等工具
l 控制内存分配的速度,限制 Goroutine 的数量,从而提高赋值器对 CPU 的 利用率。
l 减少并复用内存,例如使用 sync.Pool 来复用需要频繁创建临时对象,例如 提前分配足够的内存来降低多余的拷贝。
l 需要时,增大 GOGC 的值,降低 GC 的运行频率。