更新几篇golang文章

This commit is contained in:
2025-07-19 18:51:03 +08:00
parent 150bcef90e
commit 0cad74f895
7 changed files with 5089 additions and 1 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,326 @@
---
title: Go_map底层结构
published: 2025-07-19
description: ''
image: 'https://blog.meowrain.cn/api/i/2025/07/19/uje4vo-1.webp'
tags: [切片, Golang, Go]
category: 'Go'
draft: false
lang: ''
---
# Golang map底层数据结构
<https://golang.design/go-questions/map/principal/>
[Golang map 实现原理](https://mp.weixin.qq.com/s?__biz=MzkxMjQzMjA0OQ==&mid=2247483868&idx=1&sn=6e954af8e5e98ec0a9d9fc5c8ceb9072&chksm=c10c4f02f67bc614ff40a152a848508aa1631008eb5a600006c7552915d187179c08d4adf8d7&scene=0&xtrack=1&subscene=90#rd)
## 概述
map是一种常用的数据结构核心特征包括下面三点
- 存储基于key-value对映射的模式
- 基于key维度实现存储数据的去重
-删操作控制时间复杂度O(1)
![image-20250402212335440](https://blog.meowrain.cn/api/i/2025/04/02/n5y1Lh1743600215837401704.avif)
### 初始化方法
```go
map1 := make(map[string]int)
map2 := map[string]int{
"m1": 1,
"m2":2,
}
```
### key 类型要求
map中,key的数据类型必须是可以比较的类型,slice,chan,func,map不可比较所以不能作为map的key
![image-20250402210528197](https://blog.meowrain.cn/api/i/2025/04/02/fFJnr51743599129052146367.avif)
![image-20250402210536019](https://blog.meowrain.cn/api/i/2025/04/02/3eTMZz1743599137424628575.avif)
![image-20250402210601926](https://blog.meowrain.cn/api/i/2025/04/02/yP4UYM1743599162191602503.avif)
![image-20250402210607311](https://blog.meowrain.cn/api/i/2025/04/02/KZhyJp1743599167648587617.avif)
![image-20250402210620988](https://blog.meowrain.cn/api/i/2025/04/02/QFpAk21743599181398433044.avif)
# 核心原理
map又称为hash map算法上基于hash实现key的映射和寻址在数据结构上基于桶数组实现key-value对的存储
以一组key-value对写入map的流程进行简述
1. 通过哈希方法去的key的hash值
2. hash值对同数组长度取模确定它所属的桶
3. 在桶中插入key value对
![图片](https://blog.meowrain.cn/api/i/2025/04/02/MmAiV11743599321939050023.avif)
## hash
hash 译作散列,是一种将任意长度的输入压缩到某一固定长度的输出摘要的过程,由于这种转换属于压缩映射,输入空间远大于输出空间,因此不同输入可能会映射成相同的输出结果. 此外hash在压缩过程中会存在部分信息的遗失因此这种映射关系具有不可逆的特质.
1. hash的可重入性 相同的key必然产生相同的hash值
2. hash的离散性 只要两个key不相同不论他们相似度的高低产生的hash值会在整个输出域内均匀地离散化
3. hash的单向性 企图通过hash值反向映射会key是无迹可寻的。
4. hash冲突 由于输入域无穷大输出域有限必然存在不同key映射到相同hash值的情况这种情况叫做哈希冲突
![图片](https://blog.meowrain.cn/api/i/2025/04/02/RV0Syj1743599459574600284.avif)
## 桶数组
map中会通过长度为2的整数次幂的桶数组进行key-value对的存储
1. 每个桶固定可以存放8个key-value对
2. 倘若超过8个key-value对打到桶数组的同一个索引当中此时会通过创建桶链表的方式来化解这个问题。
![图片](https://blog.meowrain.cn/api/i/2025/04/02/X7NMOa1743599952016346994.avif)
## 拉链法解决hash冲突
首先由于hash冲突的存在不同的key可能存在相同的hash值
再者hash值会对桶数组长度取模因此不同的hash值可能被打到同一个桶中
综上不同的key-value可能被映射到map的同一个桶当中。
拉链法中,将命中同一个桶的元素通过链表的形式进行连接,因此便于动态扩展
> 只有当一个桶已经满了8 个 kv 对),并且又有新的 key 哈希到这个桶时,才会创建溢出桶,并将新的 key-value 对存储到溢出桶中,然后将该溢出桶链接到原桶的尾部。 后续再有冲突的 kv 对,也会被添加到溢出桶或者新的溢出桶中,形成一个链表。
![img](https://blog.meowrain.cn/api/i/2025/04/02/lgobAo1743600543664079674.avif)
## 开放寻址法解决hash冲突
> 开放寻址法是一种解决哈希冲突的方法,它在哈希表中寻找另一个空闲位置存储冲突的元素,也就是说,所有元素都直接存储在哈希表的桶中
>
> 开放寻址法是一种在哈希表中解决冲突的方法。当两个不同的键映射到同一个索引位置时,就会发生冲突。开放寻址法不是使用链表等额外的数据结构来存储冲突的键值对,而是尝试在哈希表本身中寻找一个空闲的位置来存储新的键值对。
![图片](https://blog.meowrain.cn/api/i/2025/04/02/GNSRsu1743600902616857141.avif)
常见开放寻址技术:
- 线性寻址: 如果在索引`i`发生冲突,线性探测会依次检查`i+1`,`i+2`,`i+3`等位置,直到找到一个空闲的槽位
- 二次探测检查 `i + 1^2``i + 2^2``i + 3^2` 等位置。与线性探测相比,这有助于减少聚集现象。
- 双重哈希: 双重哈希使用第二个哈希函数来确定探测的步长。如果第一个哈希函数在索引`i`导致哈希冲突第二个哈希函数hash2(key)用于确定探测的间隔(例如,`i + hash2(key)``i + 2*hash2(key)``i + 3*hash2(key)` 等)。
![image-20250402213515236](https://blog.meowrain.cn/api/i/2025/04/02/lsNJNR1743600915626536212.avif)
我们的golang map解决哈希冲突的方式结合了拉链法和开放寻址法。
- 桶: map的底层数据结构是一个桶数组每个桶严格意义上是一个单向桶链表
- 桶的大小: 每个桶可以固定存放8个key value对
- 当key命中一个桶的时候首先根据开放寻址法在桶的8个位置中寻找空位进行插入
- 倘若8个位置都已经被占满就基于桶的溢出桶指针找到下一个桶重复第三步
- 倘若遍历到链表尾部还没找到空位就用拉链法在桶链表尾部接入新桶并且插入key-value对
![image-20250402215431186](https://blog.meowrain.cn/api/i/2025/04/02/PB9PuR1743602071901331051.avif)
![图片](https://blog.meowrain.cn/api/i/2025/04/02/Xlpg4R1743602154258822359.avif)
## 扩容性能优化
倘若map的桶数组长度固定不变那么随着key-value对数量的增长当一个桶下挂载的key-value达到一定的量级此时操作的时间复杂度会趋于线性无法满足诉求。
**桶数组长度固定不变 + key-value 对数量持续增加 => 哈希冲突加剧 => Bucket 链表变长 => 查找/插入/删除 需要遍历长链表 => 操作时间复杂度接近 O(n) (线性)**
因此在设计上map桶的数组长度会随着key-value对的数量变化而实时调整。保证每个桶内的key-value对数量始终控制在常量级别。
扩容类型分为:
- 增量扩容
- 等量扩容
### 增量扩容
触发条件: `key-value总数 / 桶数组长度 > 6.5`的时候,发生增量扩容
扩容方式: 桶数组长度增长为原来的`两倍`
目的: 减少负载因子,降低平均查找时间
负载因子: `key-value总数 / 桶的数量`
![image-20250402225053461](https://blog.meowrain.cn/api/i/2025/04/02/exh1He1743605454120683710.avif)
### 等量扩容
触发条件: 当桶内溢出桶数量大于等于2^B时B 为桶数组长度的指数B 最大取 15),发生等量扩容。)
扩容方式: 桶的长度保持为原来的值
**目的:** 解决哈希冲突严重的问题,可能由于哈希函数选择不佳导致大量 key 映射到相同的桶,即使负载因子不高,也会出现大量溢出桶。 等量扩容旨在重新组织数据,减少溢出桶的数量。
![image-20250402231943679](https://blog.meowrain.cn/api/i/2025/04/02/m4tdlZ1743607184556640257.avif)
![image-20250402231929805](https://blog.meowrain.cn/api/i/2025/04/02/7Rrm4l1743607170611676452.avif)
### 渐进式扩容
![image-20250402233251365](https://blog.meowrain.cn/api/i/2025/04/02/8hpZdr1743607972891808021.avif)
![图片](https://blog.meowrain.cn/api/i/2025/04/02/2Cb2MO1743608023551743628.avif)
# 数据结构
## hmap
```go
type hmap struct {
count int // map中键值对的数量
flags uint8 // map的状态标志位用来指示map的当前状态正在写入正在扩容等
B uint8 // buckets 数组的对数大小2^B 是buckets数组的长度比如B是5那么桶数组的长度就是2^5 = 32
noverflow uint16 //溢出桶数量的近似值 用来判断是否需要扩容
hash0 uint32 // 哈希种子
buckets unsafe.Pointer //指向bucket数组的指针数组大小为2 ^ B如果count == 0,那么buckets可能为nil
oldbuckets unsafe.Pointer // 如果发生扩容指向旧的buckets数组
nevacuate uintptr // 扩容的时候表示旧buckcet数组已经迁移到新bucket数组的数量计数器
extra *mapextra // 可选字段用来保存overflow buckets的信息
}
```
flags: map状态标识其包含的主要状态为这里面牵扯到很多概念还没有涉及可以先大致的了解一下各自的含义
- iterator(`0b0001`): 当前map可能正在被遍历
- oldIterator(`0b0010`): 当前map的旧桶可能正在被遍历
- hashWrting(`0b0100`): 一个goroutine正在向map中写入数据
- sameSizeGrow(`0b1000`): 等量扩容标志字段
## bmap
![](https://blog.meowrain.cn/api/i/2025/04/04/Nb8mWR1743757559555396698.avif)
![](https://blog.meowrain.cn/api/i/2025/04/04/R3jihc1743757664615047610.avif)
> bmap就是map中的桶可以存储8组key-value对数据以及一个只想下一个溢出桶的指针
![](https://blog.meowrain.cn/api/i/2025/04/04/cH27qX1743757980953367677.avif)
每一组key-value对数据包含key高8位hash值tophashkey,value三部分
我们来看看bmap的内存模型
![](https://blog.meowrain.cn/api/i/2025/04/04/4iwDeb1743757807687319535.avif)
如果按照 `key/value/key/value/...` 这样的模式存储,那在每一个 key/value 对之后都要额外 padding 7 个字节;而将所有的 keyvalue 分别绑定到一起,这种形式 `key/key/.../value/value/...`,则只需要在最后添加 padding。
每个 bucket 设计成最多只能放 8 个 key-value 对,如果有第 9 个 key-value 落入当前的 bucket那就需要再构建一个 bucket ,通过 `overflow` 指针连接起来。
### tophash的作用
是key 哈希值的高8位
tophash的核心作用是**判断一个键是否可能存在于当前桶中,从而优化查询效率。**
## 溢出桶数据结构 mapextra
在map初始化的时候会根据初始数据量不同自动创建不同数量的溢出桶。在物理结构上初始的正常同和溢出桶是连续存放的正常桶和溢出桶之间的关系是靠链表来维护的。
> `mapextra` 就是在扩容时提供了一批预备的 `bmap`,然后利用 `bmap.overflow` 把它们链接起来。
```go
type mapextra struct {
overflow *[]*bmap // overflow buckets 的指针数组
oldoverflow *[]*bmap // 旧的 overflow buckets 的指针数组
nextOverflow *bmap // 指向空闲的 overflow bucket
}
```
在map初始化的时候倘若容量过大会提前申请好一批溢出桶供后续使用这部分溢出桶存放在hmap.mapextra当中
mapextra.overflow 是一个指向溢出桶切片的指针这个切片里面的溢出桶是当前使用的用于存储hmap.buckets中的桶的溢出数据。
mapextra.oldoverflow 也是一个指向溢出桶切片的指针,但是它指向的是旧的桶数组的溢出桶。
nextOverflow指向下一个可用的溢出桶
![](https://blog.meowrain.cn/api/i/2025/04/04/eZLvxe1743757352736850834.avif)
---
# 什么是哈希种子?
哈希种子(hash seed)是一个随机生成的数值,被用作哈希函数的一部分,来增加哈希值的随机性和不可预测性,可以把它理解为哈希函数的“盐”
# go map 如何根据key的哈希值确定键值存储到哪个桶中
## 哈希值的作用
- 首先,当你在 Go map 中插入一个键值对时Go runtime 会对键进行哈希运算,生成一个哈希值(一个整数)。 优秀的哈希函数应该能够将不同的键尽可能均匀地映射到不同的哈希值,以减少哈希碰撞的概率。
- 这个哈希值是确定键值对存储位置的关键。
## go map 数据结构中hmap 中B的作用
我们通过哈希值的低B位作为bucket数组的索引 来选择键值该存储到哪个bucket中。
公式 `bucketIndex = hash & ((1 << B) - 1)`
上面的公式 用来**保留 `hash` 的低 `B` 位,并将其他位设置为 0**。
![image-20250402234237409](https://blog.meowrain.cn/api/i/2025/04/02/Vtatge1743608558267235069.avif)
# key定位过程
key经过哈希计算后得到哈希值共64个bit位计算它到底要落在哪个桶的时候只会用到最后B个bit位log2BucketCount
例如现在有一个key经过哈希函数计算后得到的哈希结果是
```
10010111 | 000011110110110010001111001010100010010110010101010 │ 01010
```
而我们的B是5也就是有2^5 = 32个桶
取最后五位,也就是 **01010** 转换为10进制也就是10也就是 **10号桶**,这个操作其实是 **取余操作**,但是取余数开销太大,就用上面的位运算代替了。
接下来我们再用 **hash值的高8位**找到key在 **10号桶**中的位置 **1001011转换为10进制也就是 75**.最开始桶内还没有 key新加入的 key 会找到第一个空位,放入。
![](https://blog.meowrain.cn/api/i/2025/04/04/JcLsW91743759107613607466.avif)
![](https://blog.meowrain.cn/api/i/2025/04/04/dCIofJ1743759450720106234.avif)
# 流程
![](https://blog.meowrain.cn/api/i/2025/04/05/VUcqQy1743839544909227250.avif)
# 写入流程
写入流程:
- 进行hmap是否为nil的检查如果为空就触发panic
- 进行并发读写的检查,倘若已经设置了并发读写标记,就抛出"concurrent map writes"异常。
- 处理桶迁移。如果正在扩容把key所在的旧桶数据迁移到新桶同时迁移index位h.nevacuate的桶迁移完成后h.nevacuate自增。更新迁移进度。如果所有桶迁移完毕清除正在扩容的标记。
- 查找 key 所在的位置,并记录桶链表的第一个空闲位置(若此 key 之前不存在,则将该位置作为插入位置)。
- 若此 key 在桶链表中不存在,判断是否需要扩容,若溢出桶过多,则进行相同容量的扩容,否则进行双倍容量的扩容。
- 若桶链表没有空闲位置,则申请溢出桶来存放 key - value 对。
- 设置 key 和 tophash[i] 的值。
- 返回 value 的地址。
# 删除流程
删除流程:
- 进行并发读写检查。
- 处理桶迁移如果map处于正在扩容的状态就迁移两个桶
- 定位key所在的位置
- 删除kv对的占用这里是伪删除只有在下次扩容的时候被删除的key所占用的同空间才会得到释放。
- map首先会将对应位置的tophash[i]设置为emptyOne表示该位置被删除
- 如果tophash[i]后面还有有效的节点就仅设置为emptyOne标志意味着这个节点后面仍然存在有效的key-value对 后续在查找某个key的时候这个节点只后仍然需要继续查找
- 要是tophash[i]是桶链表的最后一个有效节点那么从这个节点往前遍历将链表最后面所有标志位emptyOne的位置都设置为emptyRest。这样在查找某个key的时候emptyRest之后的节点不需要继续查找。
> - **`emptyOne`** 表示当前 cell 是空的,但**不能保证**后面的 cell 也是空的。
> - **`emptyRest`** 表示当前 cell 是空的,并且**保证**后面的所有 cell 也是空的,直到遇到一个非空 cell 或者到达桶的末尾。
# 迭代流程
在每次对 map 进行循环时,会调用 mapiterinit 函数,以确定迭代从哪个桶以及桶内的哪个位置起始。由于 mapiterinit 内部是通过随机数来决定起始位置的,所以 map 循环是无序的,每次循环所返回的 key - value 对的顺序都各不相同。
![](https://blog.meowrain.cn/api/i/2025/04/05/TABXTR1743840105513843585.avif)

View File

@@ -0,0 +1,189 @@
---
title: Go_slice切片原理
published: 2025-07-19
description: ''
image: 'https://blog.meowrain.cn/api/i/2025/07/19/uje4vo-1.webp'
tags: [切片, Golang, Go]
category: 'Go'
draft: false
lang: ''
---
# slice数据结构
数据结构
我们每定义一个slice变量golang底层都会构建一个slice结构的对象。slice结构体由3个成员变量构成
array表示数组指针数组用于存储数据。
len表示切片长度也就是数组index从0到len-1已存储数据。
cap表示切片容量当切片长度超过最大容量时需要扩容申请更大长度的数组。
```go
type slice struct {
array unsafe.Pointer // 数组指针
len int // 切片长度
cap int // 切片容量
}
```
# 扩容原理
切片的扩容流程源码位于 runtime/slice.go 文件的 growslice 方法当中,其中核心步骤如下:
• 倘若扩容后预期的新容量小于原切片的容量,则 panic
• 倘若切片元素大小为 0元素类型为 struct{}),则直接复用一个全局的 zerobase 实例,直接返回
• 倘若预期的新容量超过老容量的两倍,则直接采用预期的新容量
• 倘若老容量小于 256则直接采用老容量的2倍作为新容量
• 倘若老容量已经大于等于 256则在老容量的基础上扩容 1/4 的比例并且累加上 192 的数值,持续这样处理,直到得到的新容量已经大于等于预期的新容量为止
• 结合 mallocgc 流程中,对内存分配单元 mspan 的等级制度,推算得到实际需要申请的内存空间大小
• 调用 mallocgc对新切片进行内存初始化
• 调用 memmove 方法,将老切片中的内容拷贝到新切片中
• 返回扩容后的新切片
```go
// nextslicecap computes the next appropriate slice length.
func nextslicecap(newLen, oldCap int) int {
newcap := oldCap // 将新容量初始化为旧容量
doublecap := newcap + newcap // 计算旧容量的两倍
// 如果所需的新长度大于旧容量的两倍,则直接使用所需的新长度
if newLen > doublecap {
return newLen
}
const threshold = 256 // 定义一个阈值,用于区分小切片和大切片
// 如果旧容量小于阈值,则直接将新容量设置为旧容量的两倍
// 这种策略适用于小切片,可以快速扩容,减少扩容次数
if oldCap < threshold {
return doublecap
}
// 对于大切片,使用更平滑的扩容策略,避免过度分配内存
// 从 2 倍增长过渡到 1.25 倍增长。 此公式给出了两者之间的平滑过渡。
for {
// 每次循环,将新容量增加 (newcap + 3*threshold) / 4
// 相当于 newcap 增加 1/4 的比例,再加上 3/4 的 threshold(256),即 192
// 这样可以在一定程度上减少内存浪费,并保证切片的增长
newcap += (newcap + 3*threshold) >> 2
// Check for overflow and determine if the new calculated capacity
// is greater or equal to the required new length.
// newLen is guaranteed to be larger than zero, hence
// when newcap overflows then `uint(newcap) > uint(newLen)`.
// This allows to check for both with the same comparison.
// 我们需要检查`newcap >= newLen`以及`newcap`是否溢出。
// 保证 newLen 大于零,因此当 newcap 溢出时,'uint(newcap) > uint(newLen)'。
// 这允许使用相同的比较来检查两者。
// 检查新容量是否大于等于所需的新长度,并且检查是否发生了溢出
if uint(newcap) >= uint(newLen) {
break // 如果新容量足够大,或者发生了溢出,则退出循环
}
}
// 当新容量计算溢出时,将新容量设置为请求的容量。
// 如果计算过程中发生了溢出,则直接将新容量设置为所需的新长度,以确保切片能够容纳所有元素
if newcap <= 0 {
return newLen
}
return newcap // 返回计算得到的新容量
}
```
# Golang 切片原理
![](https://blog.meowrain.cn/api/i/2025/01/27/STHBnZ1737969258402080877.avif)
![](https://blog.meowrain.cn/api/i/2025/01/27/L5OPBU1737969429035465587.avif)
## 扩容规律
![](https://blog.meowrain.cn/api/i/2025/01/27/my5VWv1737969803395420365.avif)
## 切片作为参数
Go 语言的函数参数传递,只有值传递,没有引用传递,切片作为参数也是如此
我们来验证这一点
![](https://blog.meowrain.cn/api/i/2025/01/27/34ZRq21737970293711745015.avif)
```go
package main
import "fmt"
func main() {
sl := []int{6, 6, 6}
f(sl)
fmt.Println(sl)
}
func f(sl []int) {
for i := 0; i < 3; i++ {
sl = append(sl, i)
}
fmt.Println(sl)
}
```
可以看到,输出的 sl 的值是不一样的也就是说f 函数没能修改主函数中的 sl 变量,而只是修改了形参 sl 变量的内容
当我们传递一个切片给函数的时候,函数接收到的其实是这个切片的一个副本,但是他们的 array 字段指向的是同一个底层数组。
这意味着,如果我们修改底层数组,是会影响到实参和形参的。
我们看下面的例子:形参通过改变底层数组影响实参
```go
package main
import "fmt"
func main() {
sl := []int{6, 6, 6}
f(sl)
fmt.Println(sl)
}
func f(sl []int) {
sl[1] = 1
sl[2] = 2
}
```
![](https://blog.meowrain.cn/api/i/2025/01/27/f395pe1737970003488259606.avif)
### 通过指针传递影响实参
```go
package main
import "fmt"
func main() {
sl := []int{6, 6, 6}
f(&sl)
fmt.Println(sl)
}
func f(sl *[]int) {
*sl = append(*sl, 200)
}
```
![](https://blog.meowrain.cn/api/i/2025/01/27/igiBeJ1737970227764617103.avif)

View File

@@ -0,0 +1,244 @@
---
title: Golang垃圾回收机制
published: 2025-07-19
description: ''
image: 'https://blog.meowrain.cn/api/i/2025/07/19/uje4vo-1.webp'
tags: [垃圾回收, Golang, GC]
category: 'Go'
draft: false
lang: ''
---
# Go GC机制
> [5、Golang三色标记混合写屏障GC模式全分析 (yuque.com)](https://www.yuque.com/aceld/golang/zhzanb#77fdf35b)
> 垃圾回收(Garbage Collection简称GC)是编程语言中提供的自动的内存管理机制自动释放不需要的内存对象让出存储器资源。GC过程中无需程序员手动执行。GC机制在现代很多编程语言都支持GC能力的性能与优劣也是不同语言之间对比度指标之一。
## 发展过程
Go V1.3之前的标记-清除(mark and sweep)算法Go V1.3之前的标记-清扫(mark and sweep)的缺点
## Go V1.3之前的标记-清除(mark and sweep)算法
![image-20240709121221919](https://blog.meowrain.cn/api/i/2024/07/09/C6W4Y71720498342584015950.webp)
接下来我们来看一下在Golang1.3之前的时候主要用的普通的标记-清除算法,此算法主要有两个主要的步骤:
- 标记(Mark phase)
- 清除(Sweep phase)
![image-20240709120731505](https://blog.meowrain.cn/api/i/2024/07/09/ANh9c11720498052447247658.webp)
![image-20240709120757145](https://blog.meowrain.cn/api/i/2024/07/09/yWrUwk1720498077557020958.webp)
> STW会对可达对象做上标记然后对不可达对象进行GC回收
![image-20240709120900088](https://blog.meowrain.cn/api/i/2024/07/09/29Wcxv1720498140387778591.webp)
> 操作非常简单但是有一点需要额外注意mark and sweep算法在执行的时候需要程序暂停即 `STW(stop the world)`STW的过程中CPU不执行用户代码全部用于垃圾回收这个过程的影响很大所以STW也是一些回收机制最大的难题和希望优化的点。所以在执行第三步的这段时间程序会暂定停止任何工作卡在那等待回收执行完毕。
### mark and sweep 算法 缺点
1. STW会让程序暂停使程序出现卡顿(重要问题)
2. 标记需要扫描整个heap
3. 清除数据会产生heap碎片
stw暂停范围
![image-20240709121953696](https://blog.meowrain.cn/api/i/2024/07/09/kMFipT1720498794174933847.webp)
从上图来看全部的GC时间都是包裹在STW范围之内的这样貌似程序暂停的时间过长影响程序的运行性能。所以Go V1.3 做了简单的优化,将STW的步骤提前, 减少STW暂停的时间范围.如下所示
![54-STW2.png](https://blog.meowrain.cn/api/i/2024/07/09/rI4lNh1720498833454407229.webp)
上图主要是将STW的步骤提前了一步因为在Sweep清除的时候可以不需要STW停止因为这些对象已经是不可达对象了不会出现回收写冲突等问题。
但是无论怎么优化Go V1.3都面临这个一个重要问题,就是**mark-and-sweep 算法会暂停整个程序** 。
Go是如何面对并这个问题的呢接下来G V1.5版本 就用**三色并发标记法**来优化这个问题.
## GoV1.5三色标记法
![image-20240709122423404](https://blog.meowrain.cn/api/i/2024/07/09/u0ZJ951720499063811507708.webp)
![image-20240709122647686](https://blog.meowrain.cn/api/i/2024/07/09/MRhIFy1720499208514108528.webp)
![image-20240709122753872](https://blog.meowrain.cn/api/i/2024/07/09/Z6DyjS1720499274479089970.webp)
![image-20240709122920596](https://blog.meowrain.cn/api/i/2024/07/09/OPgFix1720499361118341644.webp)
![image-20240709123017964](https://blog.meowrain.cn/api/i/2024/07/09/ZkEIjD1720499418393168076.webp)
![image-20240709123108479](https://blog.meowrain.cn/api/i/2024/07/09/wULnvE1720499469045471792.webp)
![image-20240709123127729](https://blog.meowrain.cn/api/i/2024/07/09/VpPh5n1720499488250837040.webp)
![image-20240709123144258](https://blog.meowrain.cn/api/i/2024/07/09/lGPm8C1720499504716064921.webp)
![image-20240709123228889](https://blog.meowrain.cn/api/i/2024/07/09/qkpbys1720499549310981229.webp)
## 三色标记法无STW的问题
我们加入如果没有STW那么也就不会再存在性能上的问题那么接下来我们假设如果三色标记法不加入STW会发生什么事情
我们还是基于上述的三色并发标记法来说, 他是一定要依赖STW的. 因为如果不暂停程序, 程序的逻辑改变对象引用关系, 这种动作如果在标记阶段做了修改,会影响标记结果的正确性,我们来看看一个场景,如果三色标记法, 标记过程不使用STW将会发生什么事情?
我们把初始状态设置为已经经历了第一轮扫描目前黑色的有对象1和对象4 灰色的有对象2和对象7其他的为白色对象且对象2是通过指针p指向对象3的如图所示。
![55-三色标记问题1.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/V3y0mh1720502068945434434.webp)
现在如何三色标记过程不启动STW那么在GC扫描过程中任意的对象均可能发生读写操作如图所示在还没有扫描到对象2的时候已经标记为黑色的对象4此时创建指针q并且指向白色的对象3。
![56-三色标记问题2.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/FKfHeh1720502103967556957.webp)
与此同时灰色的对象2将指针p移除那么白色的对象3实则就是被挂在了已经扫描完成的黑色的对象4下如图所示。
![57-三色标记问题3.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/vja2PL1720502115722049746.webp)
然后我们正常指向三色标记的算法逻辑将所有灰色的对象标记为黑色那么对象2和对象7就被标记成了黑色如图所示。
![58-三色标记问题4.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/w2ane51720502140258068700.webp)
那么就执行了三色标记的最后一步,将所有白色对象当做垃圾进行回收,如图所示。
![59-三色标记问题5.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/bzlj9c1720502156829093691.webp)
但是最后我们才发现本来是对象4合法引用的对象3却被GC给“误杀”回收掉了。
### GC误杀条件
可以看出,有两种情况,在三色标记法中,是不希望被发生的。
- 条件1: 一个白色对象被黑色对象引用**(白色被挂在黑色下)**
- 条件2: 灰色对象与它之间的可达关系的白色对象遭到破坏**(灰色同时丢了该白色)**
如果当以上两个条件同时满足时,就会出现对象丢失现象!
## 屏障机制
> 为了防止这种现象的发生最简单的方式就是STW直接禁止掉其他用户程序对对象引用关系的干扰但是**STW的过程有明显的资源浪费对所有的用户程序都有很大影响**。那么是否可以在保证对象不丢失的情况下合理的尽可能的提高GC效率减少STW时间呢答案是可以的我们只要使用一种机制尝试去破坏上面的两个必要条件就可以了。
![image-20240709132144714](https://blog.meowrain.cn/api/i/2024/07/09/2cO2yL1720502505096278545.webp)
### 强三色不变式
强制性的不允许黑色对象引用白色对象
> 破坏条件1
![image-20240709131813359](https://blog.meowrain.cn/api/i/2024/07/09/iZbHhI1720502294165051623.webp)
### 弱三色不变式
黑色对象可以引用白色对象,但是要保证白色独享存在其它灰色对象对它的引用,或者可达它的链路上游存在灰色对象
> 破坏条件2
![image-20240709132012351](https://blog.meowrain.cn/api/i/2024/07/09/SwzQBu1720502412929353413.webp)
为了遵循上述的两个方式GC算法演进到两种屏障方式他们“插入屏障”, “删除屏障”。
![image-20240709133322663](https://blog.meowrain.cn/api/i/2024/07/09/JjkcAo1720503203424780995.webp)
### 插入屏蔽
> 不在栈上使用
`具体操作`: 在A对象引用B对象的时候B对象被标记为灰色。(将B挂在A下游B必须被标记为灰色)
`满足`: **强三色不变式**. (不存在黑色对象引用白色对象的情况了, 因为白色会强制变成灰色)
```go
添加下游对象(当前下游对象slot, 新下游对象ptr) {
//1
标记灰色(新下游对象ptr)
//2
当前下游对象slot = 新下游对象ptr
}
```
这里说一下这个过程,首先因为插入屏障不在栈上使用
下面的图里面已经进行了一次三色标记外界向对象4添加对象8对象1添加对象9但是我们知道对象1在栈上所以它不会应用插入屏障也就是说这个时候对象 9不会按照插入屏障的规则设置为灰色而对象4在堆上因此它会应用插入屏障所以会把对象8设置为灰色然后我们进行第二次三色标记从灰色对象出发(对象2对象7对象8) ,找可达对象(对象3)因此将对象3设置为灰色然后对象2,7,8设置为黑色接着进行第三次三色标记从灰色对象出发(对象3)发现没有可达对象因此设置对象3为黑色这个时候我们有黑色对象: 对象1对象2对象3对象4对象7对象8.
按照常理我们这个时候应该进行垃圾回收了对吧其实不然我们这个时候要把栈空间的对象全部设置为白色然后使用STW暂停栈空间(对象1对象2对象3对象9对象5),防止外界干扰(再有对象被添加到黑色对象下)
然后我们对栈空间重新进行一次三色标记,直到没有灰色对象
过程如下:
从对象1出发设置对象1为灰色接下来看从对象1走的可达对象发现可达对象有对象2和对象9因此我们把对象2和对象9设置为灰色对象把对象1设置为黑色对象然后我们再从灰色对象出发(对象2和对象9)发现对象2可达对象3对象9没有可达对象因此把对象3设置为灰色对象对象2,9设置为黑色对象接下来从灰色对象(此时只有对象3)出发发现对象3没有可达对象设置对象3为黑色对象。至此栈里面已经没有灰色对象我们先暂停STW然后进行最后的GC回收可以发现白色对象只有 对象5对象6因此对白色对象进行清除。
至此GC三色标记并发情况下的插入屏障流程完毕
![image-20240709135123289](https://blog.meowrain.cn/api/i/2024/07/09/LKKoCr1720504284631136649.webp)
![image-20240709135153851](https://blog.meowrain.cn/api/i/2024/07/09/c9akf61720504314509112134.webp)
![image-20240709135240616](https://blog.meowrain.cn/api/i/2024/07/09/9ggDq01720504361239129518.webp)
![image-20240709135330243](https://blog.meowrain.cn/api/i/2024/07/09/brrrcs1720504410886565715.webp)
![image-20240709135410526](https://blog.meowrain.cn/api/i/2024/07/09/huazYX1720504451233838741.webp)
![image-20240709135448742](https://blog.meowrain.cn/api/i/2024/07/09/WENeFq1720504489239707269.webp)
![image-20240709135535312](https://blog.meowrain.cn/api/i/2024/07/09/AYQ3tv1720504535821911058.webp)
### 删除屏蔽
`具体操作`: 被删除的对象,如果自身为灰色或者白色,那么被标记为灰色。
`满足`: **弱三色不变式**. (保护灰色对象到白色对象的路径不会断)
```
添加下游对象(当前下游对象slot 新下游对象ptr) {
//1
if (当前下游对象slot是灰色 || 当前下游对象slot是白色) {
标记灰色(当前下游对象slot) //slot为被删除对象 标记为灰色
}
//2
当前下游对象slot = 新下游对象ptr
}
```
![72-三色标记删除写屏障1.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/YIsQlm1720506425416637589.webp)
![73-三色标记删除写屏障2.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/l7zqib1720506436481765589.webp)
![74-三色标记删除写屏障3.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/9CZbQB1720506459636243158.webp)
![75-三色标记删除写屏障4.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/jsDCqs1720506469140624748.webp)
![76-三色标记删除写屏障5.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/ccDlph1720506476790209274.webp)
![77-三色标记删除写屏障6.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/zWf7Gz1720506482597765808.webp)
![78-三色标记删除写屏障7.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/bLHxhy1720506492796935675.webp)
这种方式的回收精度低一个对象即使被删除了最后一个指向它的指针也依旧可以活过这一轮在下一轮GC中被清理掉。
### 混合屏障Go V1.8
插入写屏障和删除写屏障的短板:
● 插入写屏障结束时需要STW来重新扫描栈标记栈上引用的白色对象的存活
● 删除写屏障回收精度低GC开始时STW扫描堆栈来记录初始快照这个过程会保护开始时刻的所有存活对象。
Go V1.8版本引入了混合写屏障机制hybrid write barrier避免了对栈re-scan的过程极大的减少了STW的时间。结合了两者的优点。
![image-20240709142925523](https://blog.meowrain.cn/api/i/2024/07/09/mIzEEG1720506565775368039.webp)
![79-三色标记混合写屏障1.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/WfkvFx1720506886093721996.webp)
![80-三色标记混合写屏障2.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/mhPr4L1720506893765506689.webp)
`具体操作`:
1、GC开始将栈上的对象全部扫描并标记为黑色(之后不再进行第二次重复扫描无需STW)
2、GC期间任何在栈上创建的新对象均为黑色。
3、被删除的对象标记为灰色。
4、被添加的对象标记为灰色。

View File

@@ -11,7 +11,7 @@ lang: ''
# 参考资料
![万字图解| 深入揭秘IO多路复用](https://cloud.tencent.com/developer/article/2383534)
[万字图解| 深入揭秘IO多路复用](https://cloud.tencent.com/developer/article/2383534)
# 为什么要有IO多路复用技术

File diff suppressed because it is too large Load Diff