发布于  更新于 

Go春秋招面试记录

一、基础部分

1、golang 中 make 和 new 的区别?

使用场景区别:
make只可以用来分配以及初始化类型为slice、map,chan的数据。
new可以分配任意类型的数据,并且置为0。
返回值区别:
make返回的是slice、map,chan类型本身。
new返回的是指向该类型的指针。

2、go defer作用,多个 defer 的顺序,defer 在什么时机会修改返回值?

作用:defer延迟函数,释放资源,收尾工作;如释放锁,关闭文件,关闭链接;捕获panic;
多个 defer 调用顺序是 LIFO(后入先出),defer后的操作可以理解为压入栈中,
defer,return,return value(函数返回值) 执行顺序:首先return,其次return value,最后defer。defer可以修改函数最终返回值,修改时机:有名返回值或者函数返回指针。
参考:https://www.topgoer.cn/docs/golangxiuyang/golangxiuyang-1cmee0q64ij5p

3、讲讲 Go 的 defer 底层数据结构和一些特性?

每个 defer 语句都对应一个defer 实例,多个实例使用指针连接起来形成一个单链表,保存在gotoutine 数据结构中,每次插入defer 实例,均插入到链表的头部,函数结束再一次从头部取出,从而形成后进先出的效果。
defer 的规则总结
延迟函数的参数是 defer 语句出现的时候就已经确定了的。
延迟函数执行按照后进先出的顺序执行,即先出现的 defer 最后执行。
延迟函数可能操作主函数的返回值。
申请资源后立即使用 defer 关闭资源是个好习惯。

4、能介绍下 rune 类型吗?

相当int32
golang中的字符串底层实现是通过byte数组的,中文字符在unicode下占2个字节,在utf-8编码下占3个字节,而golang默认编码正好是utf-8。
byte 等同于int8,常用来处理ascii字符
rune 等同于int32,常用来处理unicode或utf-8字符

5、如何停止一个goroutine?

可以通过向 Goroutine 发送一个信号通道来停止它。Goroutine只能在被告知检查时响应信号,因此您需要在逻辑位置(例如for 循环顶部)包含检查。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func main() {
quit: = make(chan bool)
go func() {
for {
select {
case <-quit:
return
default: // …
}
}
}()
// …
quit < -true
}

6、Go主协程如何等其余协程完再操作/控制goroutine生命周期?

使用sync.WaitGroup用来等待一组操作完成。WaitGroup内部实现了一个计数器,用来记录未完成的操作个数。Add()是协程计数器;Done()用来在操作结束时调用,使计数减一;Wait()用来等待所有的操作结束,即计数变为0,该函数会在计数不为0时等待,在计数为0时立即返回。

7、go里面如何实现set?

Set是一个集合,里面的元素不能重复
Go map类型的key是不能重复的,因此,我们可以利用这一点,来实现一个set。value用空结构体类型,能够帮我们节省内存空间,提高性能。

8、go单元测试的规范?

  1. 单元测试文件命名规则 :名字规则为 xxx_test.go
  2. 单元测试包命令规则:package xxx_test
  3. **单元测试方法命名规则:以Test开头,参数必须是t *testing.T**,如:
1
func TestAdd(t *testing.T) { ...}

9、Go 语言中不同的类型如何比较是否相等?

像 string,int,float interface 等可以通过 reflect.DeepEqual 和等于号进行比较,像 slice,struct,map 则一般使用 reflect.DeepEqual 来检测是否相等。

10、进程、线程、协程有什么区别?

进程:是应用程序的启动实例,每个进程都有独立的内存空间,不同的进程通过进程间的通信方式来通信。是操作系统分配资源的最小单位
线程:从属于进程,每个进程至少包含一个线程,线程是 CPU 调度的基本单位,多个线程之间可以共享进程的资源并通过共享内存等线程间的通信方式来通信。
协程:为轻量级线程,与线程相比,协程不受操作系统的调度,协程的调度器由用户应用程序提供,协程调度器按照调度策略把协程调度到线程中运行。

11、什么是闭包,有什么缺陷?

闭包是指一个函数值,它引用了其外部作用域中的变量。该函数值可以访问并修改其外部作用域中的变量,即使在其声明的位置已经退出作用域的情况下也可以。
缺陷:

  1. 由于闭包引用了外部作用域中的变量,这些变量的生命周期可能会被延长,导致内存占用增加。
  2. 对于循环中的闭包,如果不小心使用,可能会导致意外的行为,如最终状态错误。

12、什么情况下会出现栈溢出?

递归调用层次太深或者局部变量过多。

13、什么是golang 的内存逃逸?什么情况下会发生内存逃逸?

答:1)本该分配到栈上的变量,跑到了堆上,这就导致了内存逃逸。2)栈是高地址到低地址,栈上的变量,函数结束后变量会跟着回收掉,不会有额外性能的开销。3)变量从栈逃逸到堆上,如果要回收掉,需要进行 gc,那么 gc 一定会带来额外的性能开销。编程语言不断优化 gc 算法,主要目的都是为了减少 gc 带来的额外性能开销,变量一旦逃逸会导致性能开销变大。

内存逃逸的情况如下:
1、方法内返回局部变量指针。
2、向 channel 发送指针数据。
3、在闭包中引用包外的值。
4、在 slice 或 map 中存储指针。
5、切片(扩容后)长度太大。
6、在 interface 类型上调用方法。

二、slice

1、数组和切片的区别

相同点:
1)只能存储一组相同类型的数据结构。
2)都是通过下标来访问,并且有容量长度,长度通过 len 获取,容量通过 cap 获取。
区别:
1)数组是定长,访问和复制不能超过数组定义的长度,否则就会下标越界,切片长度和容量可以自动扩容。
2)数组是值类型,切片是引用类型,每个切片都引用了一个底层数组,切片本身不能存储任何数据,都是这底层数组存储数据,所以修改切片的时候修改的是底层数组中的数据。切片一旦扩容,指向一个新的底层数组,内存地址也就随之改变。

2、讲讲 Go 的 slice 底层数据结构和一些特性?

答:Go 的 slice 底层数据结构是由一个 array 指针指向底层数组,len 表示切片长度,cap 表示切片容量。slice 的主要实现是扩容。对于 append 向 slice 添加元素时,假如 slice 容量够用,则追加新元素进去,slice.len++,返回原来的 slice。当原容量不够,则 slice 先扩容,扩容之后 slice 得到新的 slice,将元素追加进新的 slice,slice.len++,返回新的 slice。对于切片的扩容规则
(1)容量小于 256,2 倍扩容。
(2)原来的 slice 的容量大于或者等于 256,采用1.25 倍扩容。
低版本是1024作为分界点。
面试你要强调几个点:
为什么一开始是两倍扩容,后面是1.25倍扩容?
初始时,选择两倍扩容可以快速增加切片的容量,这样可以减少频繁扩容带来的开销。
在切片较大时,选择较小的扩容因子可以更有效地利用内存空间,可以减少内存的浪费。
为什么低版本是1024作为分界点,而高版本是256?
可能是因为在实际应用中,初始容量小于1024时,频繁扩容的情况并不是那么常见,而较小的分界点可以更好地平衡内存使用和性能。

三、map相关

1、map是否并发安全?

Go语言内建的map对象不是线程安全的,并发读写的时候运行时会有检查,遇到并发问题就会导致panic。使用sync.Map是并发安全的。

2、map 循环是有序的还是无序的?

无序, map 因扩张⽽重新哈希时,各键值项存储位置都可能会发生改变。

3、map 中删除一个 key,它的内存会释放么?

不会。当从map中删除一个key时,对应的值value会被从map中移除,但是这并不意味着内存立即被释放。Go语言中的垃圾回收器(GC)负责管理内存的分配和释放,它会周期性地检查不再被引用的内存并将其释放。当你删除一个key时,对应的值可能会被标记为不再被引用,但具体的内存回收时间取决于垃圾回收器(GC)的行为。如果你想确保某个值在删除后立即释放内存,你可以通过将其置为nil来显式地告诉垃圾回收器不再需要这个值,从而加速内存的释放。例如:

1
2
3
4
5
6
7
8
myMap := make(map[string]int)
myMap["key"] = 123

// 删除key
delete(myMap, "key")

// 将对应的值置为nil
myValue := nil

4、map底层原理以及扩容原理

(1)底层:
map底层是hash map,在算法上基于 hash 实现 key 的映射和寻址;在数据结构上基于桶数组实现 key-value 对的存储.
将一组k-v写入map的流程:
(1)首先使用hash函数获得key的hash值。hash(key1)
(2)其次将获得的hash值对桶数组的长度进行取模,确定所属桶的位置
(3)存入到桶数组对应的位置去。hash(key)%4=1,就存入到桶数组[1]中去。
(2)扩容:
桶扩容大概思路,每一次有新的数据写入的时候,map会维护一个全局的计数器来记录当前的桶数组有多长,以及当前存入map的kv有多少对,然后用kv的数量除以桶的数量可以估算出每个桶中大概承载了多少的kv数据,给定一个阈值,如果算出来的值超过阈值说明压力大了,然后就会对map发起扩容操作,新的桶数组长度为原来的两倍。然后要把老的桶上面的数据迁移到新的桶中来(集中一次性迁移肯定是不可取的,比如原来桶有百万条数据,迁移成本很大。map中才用的是渐进迁移的方式。),缓解了原来老桶的压力。
Go 语言中的 map 采用的是等量扩容增量扩容相结合的方式,根据不同的条件触发不同的扩容策略。

  • 当 map 中元素的个数大于 8 且负载因子(即键值对的数量与哈希桶的数量的比值)超过 6.5 时,触发增量扩容,哈希桶的数量翻倍。
  • 当溢出桶的数量超过一定阈值时,触发等量扩容,哈希桶的数量不变,只是将哈希桶中的数据重新分配,使得每个哈希桶中的键值对数量更加均匀。

桶数组的长度是明确的,是2的整数次幂。
每个桶里的一个节点固定放8组kv,若再来一个新的kv,则通过拉链法化解这个问题。
(3)解决hash冲突的方法,拉链法和开放寻址法。
开放寻址法:来新的数据的时候,如果此时桶数组的节点被占用了,就会按照一定的探测策略持续寻找,直到找到一个可用于存放数据的空位为止。

四、channel相关

1、channel 是否线程安全?锁用在什么地方?

是线程安全的。channel 是一种用于在 goroutines 之间进行通信的原语。Channel 提供了一种线程安全的方式来发送和接收数据。具体来说:

  1. 发送和接收操作是原子的:在向一个 channel 发送数据或从一个 channel 接收数据时,这些操作都是原子的。这意味着多个 goroutine 可以同时发送或接收数据而不会导致数据竞争。
  2. 内部使用了同步原语:在实现中,channel 使用了同步原语来保证多个 goroutine 之间的并发访问的安全性。这使得 channel 是线程安全的,可以安全地在多个 goroutine 之间进行通信。

虽然 channel 本身是线程安全的,但有时候在使用 channel 的时候可能需要额外的同步操作,特别是在涉及多个 channel 或其他共享资源时。在这种情况下,可以使用互斥锁(mutex)或其他同步原语来保护共享资源的访问,以确保并发访问的安全性。
例如,如果多个 goroutine 需要同时访问一个共享的 map,你可能需要在访问这个 map 的时候使用互斥锁来保护:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import "sync"

var (
mu sync.Mutex
sharedMap = make(map[string]int)
)

func writeToMap(key string, value int) {
mu.Lock()
defer mu.Unlock()
sharedMap[key] = value
}

func readFromMap(key string) int {
mu.Lock()
defer mu.Unlock()
return sharedMap[key]
}

2、channel 的底层实现原理?

channel 的数据结构包含 qccount 当前队列中剩余元素个数,dataqsiz 环形队列长度,即可以存放的元素个数,buf 环形队列指针,elemsize 每个元素的大小,closed 标识关闭状态,elemtype 元素类型,sendx 队列下表,指示元素写入时存放到队列中的位置,recv 队列下表,指示元素从队列的该位置读出。recvq 等待读消息的 goroutine 队列,sendq 等待写消息的 goroutine 队列,lock 互斥锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type hchan struct {
//channel分为无缓冲和有缓冲两种。
//对于有缓冲的channel存储数据,借助的是如下循环数组的结构
qcount uint // 循环数组中的元素数量
dataqsiz uint // 循环数组的长度
buf unsafe.Pointer // 指向底层循环数组的指针
elemsize uint16 //能够收发元素的大小


closed uint32 //channel是否关闭的标志
elemtype *_type //channel中的元素类型

//有缓冲channel内的缓冲数组会被作为一个“环型”来使用。
//当下标超过数组容量后会回到第一个位置,所以需要有两个字段记录当前读和写的下标位置
sendx uint // 下一次发送数据的下标位置
recvx uint // 下一次读取数据的下标位置

//当循环数组中没有数据时,收到了接收请求,那么接收数据的变量地址将会写入读等待队列
//当循环数组中数据已满时,收到了发送请求,那么发送数据的变量地址将写入写等待队列
recvq waitq // 读等待队列
sendq waitq // 写等待队列


lock mutex //互斥锁,保证读写channel时不存在并发竞争问题
}

参考:https://juejin.cn/post/7037656471210819614
https://www.topgoer.cn/docs/gozhuanjia/gochan4
现在有两个goroutine,G1往管道发数据,G2从管道接受数据。

  • 初始hchan结构体中的buf为空,sendx和recvx均为0。
  • 当G1向ch里发送数据时,首先会对buf加锁,然后将数据copy到buf中,然后sendx++,然后释放对buf的锁。
  • 当G2消费ch的时候,会首先对buf加锁,然后将buf中的数据copy到task变量对应的内存里,然后recvx++,并释放锁。

可以发现整个过程,G1和G2没有共享的内存,底层是通过hchan结构体的buf,并使用copy内存的方式进行通信,最后达到了共享内存的目的,这里也体现了Go中的CSP并发模型。

3、有缓冲无缓冲channel区别以及对空、关闭channel的读写操作

无缓冲和有缓冲区别:管道没有缓冲区,从管道读数据会阻塞,直到有协程向管道中写入数据。同样,向管道写入数据也会阻塞,直到有协程从管道读取数据。管道有缓冲区但缓冲区没有数据,从管道读取数据也会阻塞,直到协程写入数据,如果管道满了,写数据也会阻塞,直到协程从缓冲区读取数据。
channel 的一些特点 1)、读写值 nil 管道会永久阻塞 2)、关闭的管道读数据仍然可以读数据 3)、往关闭的管道写数据会 panic 4)、关闭为 nil 的管道 panic 5)、关闭已经关闭的管道 panic
空读写阻塞,写关闭异常,读关闭空0。

5、讲讲channel读写数据的流程和主要使用场景

向 channel 写数据的流程: 如果等待接收队列 recvq 不为空,说明缓冲区中没有数据或者没有缓冲区,此时直接从 recvq 取出 G,并把数据写入,最后把该 G 唤醒,结束发送过程; 如果缓冲区中有空余位置,将数据写入缓冲区,结束发送过程; 如果缓冲区中没有空余位置,将待发送数据写入 G,将当前 G 加入 sendq,进入睡眠,等待被读 goroutine 唤醒;
向 channel 读数据的流程: 如果等待发送队列 sendq 不为空,且没有缓冲区,直接从 sendq 中取出 G,把 G 中数据读出,最后把 G 唤醒,结束读取过程; 如果等待发送队列 sendq 不为空,此时说明缓冲区已满,从缓冲区中首部读出数据,把 G 中数据写入缓冲区尾部,把 G 唤醒,结束读取过程; 如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程;将当前 goroutine 加入 recvq,进入睡眠,等待被写 goroutine 唤醒;
使用场景: 消息传递、消息过滤,信号广播,事件订阅与广播,请求、响应转发,任务分发,结果汇总,并发控制,限流,同步与异步

五、Context相关

1、Contenxt用途

Context(上下文)是Golang应用开发常用的并发控制技术 ,它可以控制一组呈树状结构的goroutine,每个goroutine拥有相同的上下文。Context 是并发安全的,主要是用于控制多个协程之间的协作、取消操作。
其主要的应用 :
1:上下文控制,2:多个 goroutine 之间的数据交互等,3:超时控制:到某个时间点超时,过多久超时。

2、Contenxt数据结构

Context 只定义了接口,凡是实现该接口的类都可称为是一种 context。

1
2
3
4
5
6
  type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}

Go 的 Context 的数据结构包含 Deadline,Done,Err,Value。
Deadline 方法返回一个 time.Time,表示当前 Context 应该结束的时间,ok 则表示有结束时间,Done 方法当 Context 被取消或者超时时候返回的一个 close 的 channel,告诉给 context 相关的函数要停止当前工作然后返回了,Err 表示 context 被取消的原因,Value 方法表示 context 实现共享数据存储的地方,是协程安全的。

六、什么是 GMP?

答:G 代表着 goroutine,P 代表着上下文处理器,M 代表 thread 线程,

在 GPM 模型,有一个全局队列(Global Queue):存放等待运行的 G,还有一个 P 的本地队列:也是存放等待运行的 G,但数量有限,不超过 256 个,存放当前P即将要执行的go的一些组合。

1、GPM 的调度流程:

1.从 go func()开始创建一个 goroutine(G),新建的 goroutine 优先保存在 P 的本地队列中如果 P 的本地队列已经满了,则会保存到全局队列中

2.(G只能运⾏在M中,⼀个M必须持有⼀个P,M与P是1:1的关系。)接下来M 会从 P 的队列中取一个可执行状态的 G 来执行,如果 P 的本地队列为空,就会从其他的 MP 组合偷取一个可执行的 G 来执行。

3.当 M 执行某一个 G 时候发生系统调用或者阻塞,M 阻塞,如果这个时候 G 在执行,runtime 会把这个线程 M 从 P 中摘除,然后创建一个新的线程M(如果有空闲的M就用空闲的M)来服务于这个 P

4.当 M 系统调用结束时,这个 G 会尝试获取一个空闲的 P 来执行,并放入到这个 P 的本地队列。如果获取不到G,那么这个线程 M 变成休眠状态,加入到空闲线程中,然后整个 G 就会被放入到全局队列中

2、G,P,M 的个数问题

  1. G 的个数理论上是无限制的,但是受内存限制,
  2. P 的数量一般建议是逻辑 CPU 数量的 2 倍,在程序中通过runtime.GOMAXPROCS()来设置
  3. M 的数量(了解,记住默认10000,动态开辟的)
    1. go语言本身的限制:go程序启动时,会设置M的最大数量,默认10000.但是内核很难支持这么多的线程数,所以这个限制可以忽略。
    2. runtime/debug中的SetMaxThreads函数,设置M的最大数量
    3. 一个M阻塞了,会创建新的M。

3、M和P是一一对应的吗

   M与P的数量没有绝对关系,一个M阻塞,P就会去创建或者切换另一个M,所以,即使P的默认数量是1,也有可能会创建很多个M出来。 

work stealing(工作量窃取) 机制:当本线程没有可运行的G时,尝试从其他线程绑定的P偷取G(其他也没有就去全局拿),而不是销毁本线程。

hand off (移交)机制:当本线程因为G进行调用发生系统阻塞时,本线程释放绑定的P,把P转交给其他空闲的线程执行。

4、什么是G0和M0?

M0是启动程序后的编号为0的主线程,这个M对应的实例会在全局变量runtime.m0中,不需要在heap上分配,M0负责执行初始化操作和启动第一个G在之后M0就和其他的M一样了

G0是每次启动一个M都会第一个创建的gourtine,G0仅用于负责调度的G,G0不指向任何可执行的函数, 每个M都会有一个自己的G0。在调度或系统调用时会使用G0的栈空间, 全局变量的G0是M0的G0。

七、GC垃圾回收

细分常见的三个问题:1、GC机制随着golang版本变化如何变化的?2、三色标记法的流程?3、插入屏障、删除屏障,混合写屏障(具体的实现比较难描述,但你要知道屏障的作用:避免程序运行过程中,变量被误回收;减少STW的时间)4、虾皮还问了个开放性的题目:你觉得以后GC机制会怎么优化?

1、演进过程

Go 的 GC 回收有三次演进过程,Go V1.3 之前普通标记清除(mark and sweep)方法,整体过程需要启动 STW,效率极低。GoV1.5 三色标记法,堆空间启动写屏障,栈空间不启动,全部扫描之后,需要重新扫描一次栈(需要 STW),效率普通。GoV1.8 三色标记法,混合写屏障机制:栈空间不启动(全部标记成黑色),堆空间启用写屏障,整个过程不要 STW,效率高。

2、GoV1.3 之前的标记清除法流程:

1.启动STW,找出可达对象和不可达对象。
2.给所有的可达对象做出标记。
3.标记完之后,开始清除未标记的对象。
4.停止STW,让程序接着跑,重复循环这个过程直到程序生命周期结束。

标记清除法缺点:

  • STW,stop the world;让程序暂停,程序出现卡顿 (重要问题);
  • 标记需要扫描整个heap;
  • 清除数据会产生heap碎片(有些地址不是连着的)。

缩短STW时间:1243,先暂停STW再清除。
https://www.yuque.com/aceld/golang/zhzanb#d067ac74

3、GoV1.5 三色标记法

1. 每次新创建的对象,标记为白色。
2.遍历根节点集合一次,将遍历到的对象从白色集合放入灰色集合。
3.遍历灰色集合,将灰色节点能到达的对象从白色标记为灰色放入灰色集合,之后将此灰色对象标记为黑色放入黑色集合。
4.循环上一步,直到灰色集合中没有对象。
5.回收所有白色对象,即回收垃圾。

以上便是三色并发标记法,不难看出,我们上面已经清楚的体现三色的特性。但是这里面可能会有很多并发流程均会被扫描,执行并发流程的内存可能相互依赖,为了在GC过程中保证数据的安全,我们在开始三色标记之前就会加上STW,在扫描确定黑白对象之后再放开STW。但是很明显这样的GC扫描的性能实在是太低了。

没有STW的三色标记法会带来的问题:对象被误回收
条件:1.白色被挂在黑色下面 2.灰色此时丢了白色 这两个同时满足就会丢失对象。
image.png

解决方案,引入强弱三色不变式:
1.强三色不变式:强制性的不允许黑色对象引用白色对象。
2.弱三色不变式:黑色可以引用白色,但是白色对象要存在其他的灰色对象对它的引用,或者可达它的链路上游存在灰色对象。

为了遵循上述的两个方式,GC算法演进到两种屏障方式,他们“插入屏障”, “删除屏障”。
image.png

3.1、插入和删除屏障

插入屏障(不在栈上用,为了保证栈的速度):在A对象引用B的时候,B必须被标记为灰色。
满足强三色不变式(不存在黑引用白了,因为白的都强制变灰了)
不足:结束时需要STW来重新扫描栈。

删除屏障:被删除的对象如果自身为灰色或者白色,那么被标记为灰色
满足弱三色不变式(保护灰色对象到白色对象的路径不会断)
不足:删除写屏障会造成一个对象即使被删除了最后一个指向它的指针也依旧可以活过这一轮,在下一轮 GC 中被清理掉。

Go1.5 三色标记主要是插入屏障和删除屏障,写入屏障的流程:程序开始,全部标记为白色,
1、所有的对象放到白色集合,
2、遍历一次根节点,将遍历到的对象从白色集合放入灰色集合。
3、遍历灰色节点,将灰色节点可达的对象,从白色标记灰色,遍历之后的灰色标记成黑色,
4、由于并发特性,此刻外界向在堆中的对象发生添加对象,(以及在栈中的对象添加对象,)在堆中的对象会触发插入屏障机制,(栈中的对象不触发,)由于堆中对象插入屏障,则会把堆中黑色对象添加的白色对象改成灰色,(栈中的黑色对象添加的白色对象依然是白色,)
5、循环上一 步,直到没有灰色节点,
6、在准备回收白色前,重新遍历扫描一次栈空间,加上 STW 暂停保护栈,防止外界干扰(有新的白色会被添加成黑色)在 STW 中,将栈中的对象一次三色标记,直到没有灰色,
7、停止 STW,清除白色。
至于删除写屏障,则是遍历灰色节点的时候出现可达的节点被删除,这个时候触发删除写屏障,这个可达的被删除的节点会被标记为灰色,等循环三色标记之后,直到没有灰色节点,然后清理白色。
删除写屏障会造成一个对象即使被删除了最后一个指向它的指针也依旧可以活过这一轮,在下一轮 GC 中被清理掉。回收精度低。

4、GoV1.8 混合写屏障规则是:

1、GC 开始将栈上的对象全部扫描并标记为黑色(目的是为了之后不再进行第二次重复扫描,无需 STW)。
2、GC 期间,任何在栈上创建的新对象,均为黑色。
3、被删除的对象标记为灰色。
4、被添加的对象标记为灰色。

5、GC触发时机

主动触发:调用runtime.GC
image.png

6、GC如何调优?

image.png

八、go内存分配

image-20240308180631198