我们接下来了解一下GC原理
GC它其实Go的整个从1.0到目前1.16
它其实经历了很多的一个过程
算是不断地优化
因为我最早期用的时候
实际上它的延迟还是非常厉害的
就是整个GC的时候我们叫Stop The World
整个程序暂停
时间非常长 所以当时写代码
用了很多的一些hack的手段
比方说想尽办法减少对象的分配
然后interface也不使用
然后defer也尽量尽量少使用
等等一系列的手段
现在的话其实最新版本的Go
它的整个latency已经控制得非常好了
整个GC的暂停时间也是非常好
我们先整体了解一下
从最早它是如何演进过来的
首先我们知道现代高级编程语言
它的内存管理方式就是自动和手动
就是手动挡和自动挡
像我们C和C++可能都是自己
手动的方式去管理内存
我们在写代码的过程中
就是自己去主动地申请和释放
整个生命周期全部交给你自由控制
当然我认为手动控制在一些极端情况下
做一些高性能的代码优化的确是有些优势的
包括C C++ Rust都是类似的方式
像PHP Java Go
这些语言的话
它都是使用自动的内存管理系统
为什么会这样做
我理解就是说尤其做业务开发
甚至一些部分中间件开发
都是让我们简化编程
所以对于有GC的这种语言来说
它基本上会分为几个角色
一个我们叫应用程序
叫Mutator
然后内存堆叫Heap
Allocator就是分配器
Colloctor回收器
基本上就这么几个角色
主流的垃圾回收算法
我们叫引用计数和追踪式垃圾回收
引用计数我们很早以前应该有听说过
大家也知道引用计数它会有一些缺陷
有什么缺陷大家可以自己去搜一下
Go现在用的三色标记法
属于追踪式的垃圾回收算法的一种
跟Java的非常像
都是基于Mark & Sweep的这种实现来做的
我们对于Mark & Sweep
就是叫标记和清扫的一个实现
我们先看一下标记清扫的核心几个概念
第一个叫Stop The World
STW就是Stop The World的一个缩写
GC的一些阶段因为需要停止应用程序
让它暂停
暂停完之后
以确定当前所有对象的一个引用关系
这其实也是大家对GC这种担心的一个来源
因为它会整个让程序暂停
你的吞吐会陡降
这肯定是不行的
第二个概念叫Root
Root的话就是表示根对象
因为根对象指的是我们的程序
不需要通过其它对象就可以直接访问到的对象
比方说我们的全局对象 全局变量
或者说我们在程序运行中
我们堆栈里面的一些对象
我们叫Root对象
通过Root对象
最终可以追踪到所有其它的存活对象
比方说我们举个例子
这个地方
比方你的Root set里面某一个线程的栈上
有一个对象D包含一个子对象引用了G
我们看红色的就是标记为它们是存活的
同样的ABC它也是路径可达的
对于F或者E
因为你的栈的对象变来变去
或者说它们之间的引用关系产生一些变化
E和F其实就属于一个待回收的一些对象
通过这样的方式可以追踪到哪些对象是
存在引用关系
哪些对象不存在
或者说哪些对象是处于不被追踪到的
这些非存活对象就可以之后被垃圾 GC给回收
GC的追踪算法最典型的
Mark & Sweep的算法分为两个阶段
就是标记和清除
所以它也被会叫为
Mark & Sweep的一个垃圾回收算法
标记就是我刚刚说的通过我们的Root set
去一个个地扫
找到所有存在的引用关系
清除就是把E和F这种待回收的对象给回收掉
简单可以这么理解
严格实现它这个思路是怎么做的
第一步是Stop The World让整个程序暂停
然后我们开始Mark阶段
Mark阶段我们看看假设这个地方是你的Root set
通过Root和Root直接间接访问到的对象
来寻找所有可达的对象
我们看看Mark阶段
蓝色的是不是就是找到的这些对象
然后一个个进行标记
我们再看看清扫阶段
清扫阶段其实就是对我们的堆对象进行迭代
已被标记的对象
因为你已经被标记过了
所有未被标记的就把它放入到一个freelist
相当于是一个空闲列表
你可以用于再分配
就相当于把它们回收了
然后再start the world
我们的应用程序终于又可以重新去运行了
相当于你在程序运行中突然被暂停了
这个算法最大的问题就是GC的整个执行期间
需要把整个程序完完全全暂停
我们叫朴素的Mark & Sweep
它是一个整体的Stop The World
并且分配速度是非常慢的
然后内存碎片率也是非常高
你的整个程序暂停的时间非常久
我们看一下Go 1.1版本
Stop The World可能会达到一个秒级
这个肯定是对于线上高吞吐的
一个应用来说肯定是不能接受的
所以这个是最早期的一个实现
再看看到1.3时代
到1.3时代 我们先看一下
它叫标记Stop The World
并发Sweep这个是什么一个概念
我们就看一下
标记过程需要Stop The World
是因为对象的引用关系
如果在标记阶段做了修改
会影响标记结果的正确性
所以在标记阶段
你需要完完全全Stop The World
在这个阶段因为你的代码你的程序不能运行
所以引用关系是不会变化的
这个时候如果一旦确定了
它的整个依赖的关系
这个就是需要Stop The World在Mark阶段
Sweep阶段为什么可以并行运行
因为你的栈上一旦有个对象不可达
比方说我们的Root set E对象
红色的箭头一旦把它干掉了
你的栈接下来以后
也不太可能能够访问到它
因为你的对象已经被确定了
不再可能会被引用了
所以Mark阶段Stop The World
但是清扫阶段一定可以把它干掉
所以并发GC它又分为两层含义
第一层就是指每个Mark
和Sweep本身是多个协程执行的
也就是我们叫concurrent
你可以开多个goroutine
或者叫多个线程同时执行
这是第一层的并发含义
但是第二个并发含义更难一些
叫做我们的应用程序和垃圾回收同时运行
一个是多线程或者叫多goroutine
同时并行运行
一个叫什么
叫应用程序和垃圾回收同时运行
也叫background
就在后台去执行
第一层我们叫concurrent
这一层其实是比较好实现的
无非就是说 GC的时候
因为整体Stop The World
对象引用关系不会再改变
对Mark或者Sweep
完完全全可以把它任务分块
分在多个goroutine或者协程里面去执行
这个是非常简单的
但是对于background我们叫后台执行
也就是说应用程序
和我们的垃圾回收同时运行
这个要相对复杂得多
也就是说应用程序和Mark阶段
和Sweep阶段是可以同时执行的
这个怎么做刚才说了
我们1.3以前也就是1.1版本实现的GC
它是一个
整个过程都是Stop The World
的一个标记清扫的方式
1.3以后它分离了标记和清扫
就是标记过程Stop The World
然后清扫过程可以并发执行
background sweep是比较容易实现的
注意我们相当于刚刚说的
background其实包含两层含义
一层是标记 一层是清扫
标记刚刚说的
是需要整体Stop The World
我们看看在后台去做sweep
为什么是相对容易实现
因为我们前面提到了
Mark以后哪些对象存活
哪些对象不存活
它是确定的
需要被清扫的对象是已知的
sweep的这些对象
一定是不再被引用的对象
也就是说sweep结束前这些对象
不可能会被分配到
它不存在一种引用关系
可以再访问到它
你可以自己想象一下
如果你栈上有一个
你的当前运行的栈
本来是A指向B
你之后
相当于把A指向B这个关系给彻底干掉了
你的代码在当前的运行结构里面是
绝对不太可能会引用到不可达的一个对象
因为你不存在任何一个指针可以再指向它了
所以说sweep和应用程序可以同时共存
包括全局变量也是一样的
无论是全局还是栈
不可能访问到的这些对象
是可以安全被清理掉的
所以1.5版本在标记过程它又做了一些优化
因为我们刚才说的标记过程
是需要整体Stop The World的
1.5它又做了一些小优化
做到了标记和清扫都是可以并发执行的
在标记阶段也不是说完全并发
它需要一小段的Stop The World
来做一些GC的准备和栈的re-scan
什么叫栈的re-scan
或者说GC准备工作又做了哪些工作
我们再来看一下
1.5的核心实现三色标记
三色标记实际上是对标记清除法的一个改进
因为标记清除法它在整个执行时间
需要比较长的一个Stop The World
从1.5开始演进为就是三色标记
初始化
我们先可以把所有的对象理解为它是一种白色的
之后我们一样的去把Root set
就是我们的Root加入到一个待扫描的队列
其实进入到待扫描队列
我们就把它认为是灰色
其实从源码层面是没有区分颜色的
只是原码层面其实就是说
把进入待扫描队列的这些对象
理解为是一种灰色
之后我们使用并发的goroutine
去扫描队列中的这些指针
如果这些指针还引用了其它的对象
或者说引用的其它的指针
被引用的这些指针也会被加入到队列
被扫描的对象之后变成黑色
所以白色对象我们可以先理解为
它是一开始是属于一个潜在的垃圾
内存可能会被垃圾回收
黑色对象叫活跃的对象
包括不存在任何引用外部指针的对象
以及从根对象可达的对象
垃圾回收不会扫描这些对象的子对象
还有一种叫什么
叫活跃的对象
灰色对象也叫活跃对象
就是因为存在指向白色对象的外部指针
垃圾收集器可能会是扫描这些对象的子对象
我们看一下这张图 这个可能比较绕
一开始我们看一下全是白色
然后前面的A F是先可触达的
因为我们整个Root set就两个指针A和F
把它A和F加入到灰色对象
我们再看一下灰色对象定义
因为存在指向白色对象的外部指针
也就是说Root set触达到了A和F
所以说先把它们加入到灰色对象待扫描
然后A和F之后再扫描BCD
BCD就变成灰色
之前的A和F就加入到黑色对象
所以我们再看一下
一开始是AF先进灰色
然后BCD再进灰色
然后AF变成黑色
然后之后bcd在进入黑色
所以最终 EGH
我们可以看一下EGH这几个对象
白色对象叫潜在垃圾
因为没有任何对象可以再指向它了
因为你的栈上的指针没有任何指向G H E了
是不是它就是可以被回收掉
大概是这么一个意思
就这几种颜色
我们再看看它的一个阶段
一开始垃圾回收器它从Root开始扫描
然后跟随指针递归整个内存空间
分配于noscan的span对象
内存原理里面讲过
它是其中分为scan和noscan
noscan其实就是不包含指针的
所以它不需要扫描
我看我们这个图所有noscan的
你可以完全忽略它
只有scan这些对象这些
这些span它才会去进行扫描
其实这个过程
一开始你看这个地方
我们最终其实也就能处理谁处理S1
也就是说我们这个代码里面一开始
这些都是noscan
所以说我们可以把它忽略掉
那S1其实又会把它加入到一个待扫描的队列
相当于有个goroutine会把它入队
我们把这些对象给入队
大概是这么个意思
我们再看看完整的一个染色过程
一开始都是潜在垃圾待回收的一个对象
第二步是什么
我们从根节点主要是从我们的栈上
goroutine栈上有一些
局部变量指向的一些堆对象
这些对象肯定就是存活的
所以说栈上指向的这些heap
还有这些全局变量
全局变量也是被扫描的
被染色为灰色
也就是这个地方S1被染色为灰色
这是第一步
先把根节点的找一找
S2这种noscan对象
我们就不用去扫它 不用管它
我们再看一下第二步
进入一个灰色对象
选定一个灰色对象
就是让它标记为黑色
因为你刚刚已经加入到待扫描队列了
再看它是不是存在子对象
所以说这个时候我们再把S2是不是
我们再递归把它再标记为灰色
再下个阶段其实就是遍历这个对象的所有指针
发现它没有其它子对象了
所以再把它标记为黑色
直到它所有对象被染色为黑色
noscan的意思就是指它没有其它的子对象
你不用再往下扫描了
本身它也是要被回收的
比方说你申请的一个对象
它就是一个int指针
它不存在指向其它的子对象
意味着你不需要继续向下去扫描了
但是它本身要被回收的
S2因为是不可触达的
白色的这一坨
这个对象相当于可以被回收的
标记结束以后
黑色对象表示的是内存中正在使用的
而白色的就是待回收的
由于我们这里有struct2
因为这个struct2我们看是白色的
它可能是因为是在匿名函数中创建的
并且无法从之前的栈上再去访问
因为你的匿名对象运行完了
匿名函数中创建的
你可以自己再想象一下
你的程序如果继续往下执行
在匿名函数创建的对象你有办法引用到吗
你是绝对引用不到的
所以说你是无法从栈上访问到它
所以它始终保持为白色
它是可以被清除的
在颜色的所谓的黑色 白色 灰色
其实在runtime的源码里面
它其实表示的是每一个span
它在内部有个叫
gcmarkBits的一个位图属性
该属性表示跟踪扫描的一个情况
如果扫描标记为1就是什么
就是它是一个活跃的对象
为0的话就是一个不活跃的对象
可以被回收的
大概是这样的
我们了解完之后
我们再看看叫什么写屏障
1.5版本在标记过程中使用了三色标记
我们前面介绍过原理
回收过程主要有4个阶段
标记和清扫都是并发执行的
在标记阶段的前后
它需要Stop The World一定的时间来
做一个GC的准备工作和栈的re-scan
这里面就会讲
为什么会去做一个re-scan的操作
就是因为算式写屏障的一些缺陷
1.5我们因为刚刚说了
它能保证你的
清扫阶段和应用程序同时可以并发执行
你要保证同时执行
也就是说在标记过程中
增量的一些引用关系的变化
你要能够识别出来
所以说它要满足两种三色不变性
其中的任意一种
第一种叫强三色不变性
黑色对象是存活对象
白色对象是会待回收的一个对象
黑色对象是不会指向白色对象的
因为你黑色对象如果指向白色对象
意味着一个活跃对象
指向了一个待回收对象
你去访问的时候一定会报错
所以说黑色对象它不能指向白色对象
它只能指向灰色或者是黑色对象
也就是说这条黑色对象的
下游链路全部是活跃的
第二个叫弱三色不变性
就是说黑色对象如果你要能指向白色对象
意味着一定存在另外一条路径
存在另外一个依赖关系
这个依赖关系是什么
一个灰色对象经过了各种链路
最终可以到达白色对象
这个叫弱三色不变性
如果黑色对象能够访问白色对象
前提是什么
是有一个灰色对象有一条链路可以到达它
有这条链路到达它意味着
灰色对象它不是会继续迭待扫描吗
也就是这个对象最终会变成什么
会变成灰色 白色最终会变成黑色
所以黑色再去依赖灰色的时候
就不会存在一个问题了
这两种叫强三色和弱三色不变性
我们去看一下写屏障
假设一开始
我们这个图里面B存在一个依赖关系
指向C
它是一个灰色对象
前面讲了
如果这个时候
我新增加了一个A对象指向C
可能是栈上的一个A变量
指向了C
这个时候B指向C
我们可以理解为B可能是堆上的一个对象指向C
之后把对象这个关系给干掉了
这个时候保留了一个栈上的A指向C
是不是 如果这个C还是白色
意味着它待回收
这个时候就是意味着什么
出现了一个错误
因为活跃对象引用了一个不活跃对象
你肯定就会被回收掉
那访问就会报错了
就会出现所谓的叫对象丢失
要防止这个现象的发生
我们之前知道最简单的做法是什么
直接Stop The World
禁止在用户程序运行的时候
对对象的一个引用关系进行一个干扰
但是Stop The World我们知道
耗费的时间非常长
可能这个时候吞吐也会下降的非常厉害
有没有办法减少STW的时间
也就是我们刚刚写屏障
它到底要解决一个什么问题
Go 1.5它就是使用了一个写屏障
它的原理是什么呢
插入屏障
我们叫写屏障也叫插入屏障
拦截的是将白色指针插入黑色对象的操作
也就是说如果A要指向C
我要拦截这个操作
我们再回到之前的case
还是一样的 B指向C
这个时候A要插入操作
有个A指向C的一个插入操作
就是A引用了C
这时候我们把它拦截下来之后
我就会把C自动变成黑色
是不是通过这样的一个拦截操作
A指向C这个代码里面现在被我拦截了一个动作
就是说如果你去改依赖关系
我会自动把它改一个颜色
我自动把它加入到待扫描的列表里面
所以A这个时候
指向C的时候C变成灰色
这个时候我们前面讲了
B指向C的关系干掉了
B可能是堆上的一个对象的引用关系
我把它干掉了
是不是最终ABC都是黑色的
都是活跃对象
没有任何问题
这个是写屏障的一个原理
但是我们知道如果你在栈上做
拦截的写操作
代码实现复杂度非常高
并且性能下降非常厉害
因为根据局部性的原理
我们栈上的操作其实是很密集的
有函数的传参调用栈
局部变量 协程栈等等一系列的操作
如果你搞写屏障
可想而知
在GC阶段性能下降肯定是非常厉害的
复杂度和性能是越不过去的坎
我肯定要想别的办法
也就是说我既然不在栈上做拦截操作
不做这个Stop The World
又不想做Stop The World
那我必然要补偿之前出现这种case
因为你在运行过程中
你的栈上的这些刚刚说的A指向C这个动作
它可能就是会发生的
并且B删除C的依赖关系
它这个动作是会发生
也就是说A指向C这个动作你不拦截
意味着C不会变成灰色
你就怎么办
我们前面讲到的就是需要一个重扫
re-scan重扫描栈的这么一个步骤
重扫描栈为什么需要重扫描栈
就是因为在你的过程中
我们的写屏障在1.5的代码里面
它只针对的是堆上的这些指针
栈上是不管的
不管你就得在标记结束阶段需要重扫
如果重扫
意味着栈上的这些全局指针或者栈
因为你的Mark阶段和你的应用程序同时并行
可能有新的对象分配或者指针赋值
这个时候就需要重新全部把
这个栈上的这些变化重新来一发
这个时候是需要Stop The World的
按照标记结果再去最终回收白色对象
这个叫1.5的一个缺陷
是需要重扫描
这个时间可能会耗到10~100毫秒之间
因为我们如果有很多goroutine
你要回扫re-scan整个栈其实要花很多时间
我们再回到写屏障看看它的伪代码
这个writePointer的这个
slot表示的是当前对象的一个下游
ptr是待指向的新的对象
我这个操作是什么意思
过去的老的对象的slot
写入屏障相当于是
因为我A到C插入了一个新的关系
我要把新的指针标灰
然后把老的下游的slot信息指向新的ptr
这个就是它的一个伪代码的逻辑
我们再看删除屏障
我们先看看删除屏障
它还有一种实现另外一种算法
删除屏障它也是去拦截一个写操作的
但是它是通过叫弱三色的
弱三色的不变性来完成这个动作的
也就是说
如果你要让A能够指向一个白色对象
意味着一定存在一个灰色对象能够引用它
就是我要保证这个链路不断
我们看一下刚刚那个逻辑还是一样的
B指向C有一个这样的一个引用关系
然后这个时候我A指向C
因为我实现的是删屏障
C还是白色
因为它不是写屏障
现在是另外一种算法
这个时候重点来了
B如果把C的依赖关系给删掉
C就会被变成灰色
它是在保证灰色对象到白色对象路径不会断
这个不会断
如果你删除了 我就把你变成灰色
所以也是间接完成了不会断
但是这个做法会有一个问题
如果A到C
就是A这个栈上变量指向C这个步骤没有
你B单纯的堆上一个引用先把它给干掉
C原则上是可以被回收的
但是因为存在删屏障这样的一个代码逻辑
所以C还是会变灰
也就是说它的回收精度变低了
它这个时候要到下一轮GC
才能把C对象回收掉
它的伪代码大概是这个意思
就是说
如果我的C对象是灰色或者是白色
C对象我就会把它染色成灰色
然后再把我的过去的下游的slot的信息
指向新的ptr
这个ptr可能有两种情况
一种是B指向了新的D
还有一种情况是B指向一个nil
所以说这里是两种情况
B如果删掉C的情况下
会把老的C对象变成灰色
这个是删屏障
Go 1.8它结合了叫混合屏障
它结合了删除屏障和写屏障这两种算法
我先看它的伪代码
删屏障应该是第一行
也就是说删屏障是
让过去的老的对象变成灰色
写屏障是让新的对象变成灰色
一个是保证老的对象变成灰色
就是删除的 把老的变成灰色
一个是把新的变成灰色
最终还是把slot
就是当前的下游的指向
改成新的指向
这个就是一个混合屏障的逻辑
插入屏障和删除屏障各有优缺点
写屏障或者叫插入屏障
我们之前知道
它在标记过程不需要STW可以直接开始
然后可以并发执行
但是它的结束的时候需要Stop The World
然后来重新扫描栈
这个是以前的1.5的一个问题
删屏障它需要在GC开始的时候
先Stop The World
然后它要确定所有的依赖关系
这个过程会Stop The World
一个是结束的时候会Stop The World
一个是开始的时候会Stop The World
混合写屏障它实现的是叫变形的一个弱三色不变性
同时保证黑色对象引用白色对象
引用白色对象你就怎么样会黑色引用白色
是不是写屏障就自动把它变灰
同时因为你存在删除写屏障也就是白色对象
因为处于灰色对象的保护状态
这样的话就满足了一个弱三色的不变性
同样的混合屏障它为了性能考虑
它也是只在堆上去做这个过程
只在堆上开启写屏障
这样的一个步骤
我们可以想象它有几种场景
因为只在堆上做
我们可以想象一下栈上肯定是黑色的
因为栈上你依赖它
你一定是黑色的
我们看看堆上的情况
如果我的栈上有一个A
指向一个堆上的B
这个栈接下来运行的时候
它不再依赖B
B有可能就是待回收对象
就是它被回收掉是没问题的
但是如果存在一个
堆上的对象可能是 D指向B
我们可以想象一下
D指向B如果是新加的D指向B
它自然而然会把它变成什么
如果它是个白色对象
自然而然会把它变成黑色
另外一种情况
如果你把堆上的对象指向它的关系给删掉了
它也会变成灰色
所以说不管你的栈上的一个变量
有没有指向它 都不怕
栈上对象有没有指向它其实都不怕
如果指向它没关系
它天然就是一个灰色对象
如果不指向它也没关系
还有一种情况就是删除最后一条依赖的时候
能够把它变灰也没关系
无非就是栈上有一个
箭头指向这个对象
和堆上有个箭头指向对象指向它
所以说栈上指不指向它 我其实都不怕
一个是新增的插入
依赖我会变成灰色
一种是删除的时候我会把它变成灰色
所以说最终它都不会断
所以栈上其实已经不需要重新扫描了
当然了还有一种情况
堆上新申请的对象
天然要把它变成黑色
因为你刚申请的对象如果是个白色
然后栈上突然有个指向它那肯定会出错
所以新申请的对象立马要变成黑色
栈上依不依赖它都没关系
所以这样的话不需要再去扫描整个栈了
通过这样的一个逻辑
整个GC的效率也变得非常高
这个就是1.8以后的一个混合写屏障的实现原理
我们再来看一下一个清扫阶段
清扫阶段
因为我们标记完以后
所有对象你相当于要把它
相当于回收以后那些对象是不是把它清零
相当于把它清零
这个清零的操作
你如果让它挨个整个去跑的话
肯定要耗费非常长的时间
我们再看一下
我们之前说了
我们把扫描成功以后
是有个叫gcmarkBits的这么一个bitmap
相当于1表示的是存活
0表示的不存活
有了gcmarkBits
我们之前也知道在span中也有一个
allocBits 是标记当前这个对象是否被分配的
1表示的是已分配
0表示的是未使用或者是未分配
我们知道以前是有一个allocBits
新的又有一个gcmarkBits
是不是把它们两个相互替换一下
就是把最新的
GC Mark扫描完之后的gcmarkBits
替换成你之前的allocBIts
就是完成了一个对象回收
你可以理解为通过这样的一个方式
达到了一个
内存的对象的重新利用的这么一个操作
其实我们再看这个图 其实我们相当于
一开始我们把1标记为是使用的
然后我们其实就是把它完全进行一个替换操作
其实就是把它完成一个替换操作
现在刚刚说了
你要去挨个span去清理
挨个span做替换操作
肯定是很花费时间的
Go它提供两种方式来清理内存
一种就是在后台在我们的runtime启动的时候
它开了一个叫bgsweep的goroutine
一开始可能是休眠的
一旦它进入清扫的时候
它就会挨个mspan进行处理
这是第一种策略
第二种策略就是叫lazy触发
因为当我们的goroutine尝试在堆内存
分配新内存的时候
在你申请新内存的时候
来帮你做掉这个操作
这样的话其实就是把你整个清扫的过程
打散到每一次内存分配的时候
就是我们叫lazy触发
一个是主动触发 一个叫lazy触发
lazy触发的
第二种方式我们叫on the fly去执行
这种做法其实它在源码中
就是简单来说你的mcache里面
你的现在P的mcache那些对象
很难追踪哪些已经被扫过
哪些没被扫过
这时候非常简单
先把它一股脑全部推到mcentral
推到mcentral相当于把它加入到什么
我们之前不是有个nonempty和empty两个队列
nonempty是没使用的
一股脑把它们全推到nonempty上
nonempty链表上
然后我下次再去要一个申请一个span的时候
再触发Sweep的操作
是不是相当于mcache没有任何span对象
相当于我下次再要一个span的时候帮你触发
替换操作
相当于是不是在打散到每一次内存申请和分配
然后其实后台还有一个worker在慢慢地挨着扫
虽然会花费一些时间
所以说我们可以想象一下
如果在一个GC周期里面
在清扫过程中又到下一轮GC
你还没有清扫完
这个时候其实runtime就会
在Mark的阶段开始前
它会协助把清扫的剩余工作赶紧干完
因为要开始下一轮的GC了
这个就是大概的一个原理
我们再看一下Stop The World
Stop The World
简单来说我一开始要把所有的goroutine
全部暂停
然后要把所谓的混合写屏障要打开
要把混合写屏障打开
其实就是改个全局变量
让所有的goroutine都可见的话
你肯定是要把所有人全部暂停了
所有全部暂停
我们所谓的抢占
1.14以前的抢占
我们知道是通过一个叫stack preemptable
的一个标记去抢占的
这个时候如果你是代码for循环 死循环
那种是无法被暂停的
这个时候程序在GC的时候会彻底卡死
因为你触发morestack是在任何一个函数
就是你a调b调c的时候
在任何一个地方都有可能会触发morestack
所以这个标记需要一小点的时间
让所有的goroutine才能够悬停
这个是以前的
我们也知道现在
其实1.14以后支持一个信号抢占
所以比以前相对完善一些
整个GC整个stop的时间又会变得更短
stop阶段其实就是让大家
所有的goroutine全部暂停
然后把这些线程全部加入到idle list
就是大家全部不干活了吗
M就全部放到idle list
然后goroutine本身把它们全部放到
全局列表去进行一个等待操作
GC的整个叫起搏器pacing的一个算法
它其实就是我们什么时候会触发GC
其实我们知道每两分钟会强制触发一次
这个是由sysmon来触发的
还有一个策略就是通过配置
可以去改GC的一个百分比
这个百分比相当于是告诉你
在下一次垃圾回收启动之前
你可以分配多少新内存的一个占比
比方说你设置100%意味着垃圾回收完成后
标记为活动的堆内存的内存量
你可以增加100%
就是活跃的堆内存你可以再增加100%
假设现在是2MB In-Use
你可以再用到2MB
相当于一共是4MB
超过100%它就会触发
我们也在想
我们某种情况下其实是只会使用
25%的资源来进行一个Mark操作
如果你这个时候你的内存的增长特别的快
发现你的标记的整个过程太长了
你的新对象增长太快
这个时候怎么办
其实它又会限定你的goroutine的并行度
你看以前只影响25%
现在其实影响50%
也就是这50%
你的程序是不是跑得更慢一些
吞吐下降了
同时这个MA就是MarkAssistant
就是叫标记的协助者
协助你一块进行标记
相当于现在用50%的P
或者叫50%的M来进行一个标记的过程
加速标记
这个就是整个GC的细节
其实我没有涉及到太多的源码
因为你了解完整体的一个原理之后
我们再去对着源码再去看
可能效果会好一些
然后其实GC的代码 内存原理代码
目前其实是比较乱的
整个代码在Go runtime里面
这个就是要大家可能
对着一个新版本去自己撸一撸了
因为每个版本变化实在是太大了
我也跟不过来了
但是在整体大概的原理是这些了