您现在的位置是:首页 >学无止境 >Golang题目总结网站首页学无止境
Golang题目总结
1. slice底层数据结构和扩容原理
- 数据结构
Go 的 slice 底层数据结构是由一个 array 指针指向底层数组,len 表示切片长度,cap 表示切片容量。 - 扩容原理
(1)扩容思路:对于 append 向 slice 添加元素时,若 slice 容量够用,则追加新元素进去,slice.len++,返回原来的 slice。当原容量不够,则 slice 先扩容,扩容之后 slice 得到新的 slice,将元素追加进新的 slice,slice.len++,返回新的 slice。
(2)扩容规则:当切片比较小时(容量小于 1024),采用较大的扩容倍速进行扩容(新的扩容会是原来的 2 倍),避免频繁扩容,从而减少内存分配的次数和数据拷贝的代价。当切片较大的时(原来的 slice 的容量大于或者等于 1024),采用较小的扩容倍速(新的扩容将扩大大于或者等于原来 1.25 倍),主要避免空间浪费(网上其实很多总结的是 1.25 倍,那是在不考虑内存对齐的情况下,实际上还要考虑内存对齐,扩容是大于或者等于 1.25 倍)。
2. 数组和切片的区别
在此之前,先理解下 引用和 指针 区别:
引用可以是一个变量或者一个结构体,若是结构体,它的属性可以为指针类型,那么此后,每个引用的副本中都有该指针类型的属性,而且它们都是指向同一个内存块。
指针是 一个存储内存块的地址 的变量。
- 相同点
(1)只能存储一组相同类型的数据结构
(2)都是通过下标来访问 - 不同点
(1)数组定长,而切片可扩容
(2)数组作为参数传递时传递数组副本,而切片作为参数传递时传递的是指向底层数组中的指针
(3)切片只能通过make函数进行初始化
3. 能介绍下 rune 类型吗?
前言:golang中string底层是通过byte数组实现的(所以获取字符串长度是按照字节来的)。中文字符在unicode下占2个字节,在utf-8编码下占3个字节。(golang默认编码是utf-8)。
介绍:相当于 int32。rune 是用来处理unicode或utf-8字符的(byte用来处理ascii字符)。举例(使用
[ ] rune来接受字符串时,它能够正确获取字符串长度。)
4. 调用函数传入结构体时,应该传值还是指针?
传值会拷贝整个对象,而传指针只会拷贝指针地址,指向的对象是同一个。传指针可以减少值的拷贝,但是会导致内存分配逃逸到堆中,增加垃圾回收(GC)的负担。
一般情况下,对于需要修改原对象值,或占用内存比较大的结构体,选择传指针。对于只读的占用内存较小的结构体,直接传值能够获得更好的性能。
5. 调用函数传参(slice、map、chan)时候,传的是什么?
golang中所有函数参数传递都是传值,slice、map和chan看上去像引用只是因为他们内部有指针或本身就是指针而已。(slice其实是一个含有指针的结构体,而map和slice本身就是一个指针)
6. 讲讲 Go 的 select 底层数据结构和一些特性?
概念:go 的 select语句会依次检查每个case分支,如果其中有一个通道已经准备好,就会执行相应的操作。如果有多个通道都已经准备好,select语句会随机选择一个通道执行相应的操作。
底层数据结构:select语句的底层数据结构是一个select结构体,它包含了多个case分支和一个默认分支。
特性:
(1)case语句必须是一个channel操作。
(2)select中的default子句总是可运行的
(3)select语句可以阻塞等待通道操作。
(4)如果有多个case都可以运行,select会随机公平地选出一个执行。
(5)所有channel表达式都会被求值
7. 讲讲 Go 的 defer 底层数据结构和一些特性?
每个defer语句都会创建一个defer结构体,并将其添加到当前函数的defer链表中。当函数返回时,Go运行时会依次执行defer链表中的函数,直到链表为空为止。这个过程是在函数返回之前执行的,因此可以保证被延迟执行的函数在函数返回之前被执行。
defer 的规则总结:
(1)延迟函数执行按照后进先出的顺序执行,即先出现的 defer 最后执行。
(2)延迟函数可能操作主函数的返回值。
(3)申请资源后立即使用 defer 关闭资源是个好习惯
8. map 的数据结构是什么?是怎么实现扩容?
数据结构:map的底层数据结构是hmap,hmap有多个bmap桶,每个bmap桶包含一个哈希链表,哈希链表中的每个元素都包含一个键值对。
解决哈希冲突:当向map中插入一个元素时,Go运行时会先计算元素的哈希值,然后根据哈希值找到对应的桶。如果桶中已经存在一个元素,那么新元素会被插入到链表的头部;
怎么扩容:Go 会创建一个新的 buckets 数组,新的 buckets 数组的容量是旧buckets数组的两倍(或者和旧桶容量相同),将原始桶数组中的所有元素重新散列到新的桶数组中。这样做的目的是为了使每个桶中的元素数量尽可能平均分布,以提高查询效率。旧的buckets数组不会被直接删除,而是会把原来对旧数组的引用去掉,让GC来清除内存。在map进行扩容迁移的期间,不会触发第二次扩容。只有在前一个扩容迁移工作完成后,map才能进行下一次扩容操作。(注意:以上的搬迁过程为渐进式搬迁的策略)
扩容时机:
(1)当装载因子超过6.5时,扩容一倍,属于增量扩容;
(2)当使用的溢出桶(bmap中有溢出桶这个属性)过多时,重新分配一样大的内存空间,属于等量扩容;
9. slices能作为map类型的key吗?
在golang规范中,可比较的类型都可以作为map key。
故不能作为map key 的类型包括:
(1)slices
(2)maps
(3)functions
10. map 使用注意的点,是否并发安全?
- 注意的点:
(1)map的键必须是可比较的类型,否则会在编译时报错。
(2)map是无序的,不能保证遍历的顺序和插入的顺序一致。
(3)map的值可以为任意类型,但键必须是可比较的类型。
(4)在并发环境下,map不是并发安全的,需要使用互斥锁等机制进行保护。 - 解决并发安全
(1)sync.Map —— 可以安全地在多个goroutine之间并发访问(在使用sync.Map时,需要注意它的一些限制,例如不能使用range遍历、不能在Load和Store方法中传递指向map的指针等)。
11. map 中删除一个 key,它的内存会释放么?
golang的map在key被删除之后,并不会立即释放内存。将map设置为nil后,内存被释放。
12. 解决对 map 进行并发访问?
- sync.Map —— 可以安全地在多个goroutine之间并发访问
- 对map进行并发访问时,需要使用锁来保证并发安全。常用的锁包括互斥锁(sync.Mutex)和读写锁(sync.RWMutex)
13. nil map 和空 map 有何不同?
nil map 未初始化,空map是长度为空
14. context 结构是什么样的?context 使用场景和用途?
浅入:context通知一个或多个goroutine停止。( context.WithCancel 函数)
func TestContext(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
go worker(ctx, "zhang")
go worker(ctx, "li")
time.Sleep(5 * time.Second)
cancel()
time.Sleep(1 * time.Second)
fmt.Println("all the workers take a rest")
}
func worker(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Println("worker stopped")
return
default:
fmt.Println("working...")
time.Sleep(time.Second)
}
}
}
其余细节待更
15. channel 的底层实现 (数据结构)
type hchan struct {
qcount uint // 循环队列元素的数量
dataqsiz uint // 循环队列的大小
buf unsafe.Pointer // 循环队列缓冲区的数据指针
elemsize uint16 // chan中元素的大小
closed uint32 // 是否已close
elemtype *_type // chan 中元素类型
sendx uint // send 发送操作在 buf 中的位置
recvx uint // recv 接收操作在 buf 中的位置
recvq waitq // receiver的等待队列
sendq waitq // senderl的等待队列
lock mutex // 互斥锁,保护所有字段
}
- 回复:底层是,qcount 当前队列中剩余元素个数,dataqsiz 环形队列长度,即可以存放的元素个数,buf 环形队列指针,elemsize 每个元素的大小,closed 标识关闭状态,elemtype 元素类型,sendx 队列下标,指示元素写入时存放到队列中的位置,recvx 队列下标,指示元素从队列的该位置读出。recvq 等待读消息的 goroutine 队列,sendq 等待写消息的 goroutine 队列,lock 互斥锁,chan 不允许并发读写。
- 注意:循环队列好处为一个数据被发送之后,其余数据不用移动。
16. 向 channel 写入数据和从 channel 读出数据的流程是什么样的?
- 向channel写入数据:
若 读出等待队列不为空,则把数据发送给 读出等待队列的第一个goroutine,并唤醒。
若 读出等待队列为空,若有缓冲区,则将数据写入缓冲区。若无缓冲区,将走阻塞写入的流程,将当前goroutine加入写入等待队列。并挂起等待唤醒。 - 从channel读出数据:
若 写入等待队列不为空,则从写入等待队列的第一个goroutine中读出数据,并唤醒。
若 写入等待队列为空,若有缓冲区,则从缓冲区读出数据。若无缓冲区,将走阻塞读出的流程,将当前goroutine加入读出等待队列。并挂起等待唤醒。
17. channel特点
- 关闭的管道读数据仍然可以读数据,但不可以写入数据(会panic)
- 向nil 管道读写会永久阻塞
- 关闭为 nil 的管道 panic
- 关闭已经关闭的管道 panic
18. channel有无缓冲区的区别
- 管道没有缓冲区,从管道读数据会阻塞,直到有协程向管道中写入数据。同样,向管道写入数据也会阻塞,直到有协程从管道读取数据。
- 管道有缓冲区但缓冲区没有数据,从管道读取数据也会阻塞,直到协程写入数据,如果管道满了,写数据也会阻塞,直到协程从缓冲区读取数据。
19. channel 是否线程安全?锁用在什么地方?
- 为什么设计成线程安全? 不同协程通过channel通信,所以channel的使用场景是多线程下,故需保证数据的一致性。
- 如何实现线程安全? channel的锁是通过hchan结构体中的mutex字段实现的,它是一个互斥锁,用于保护channel的读写操作。(当一个goroutine进行读写操作时,它会先获取该锁,然后进行相应的操作,最后释放锁。在进行读写操作时)
- 锁用在什么地方? 锁的作用是保护channel的状态(容量、是否已关闭等信息)和 缓冲区
20. 有那些方式可以安全的读写共享变量?
- 将共享变量的读写放到一个 goroutine 中,其它 goroutine 通过 channel 进行读写操作。
- 可以用个数为 1 的信号量(semaphore)实现互斥
- 将共享变量的读写放到一个 channel 中
- 加锁(例如通过sync包下的Mutex)
- 通过原子操作(例如通过sync包下的atomic)
22. Mutex 是悲观锁还是乐观锁?悲观锁、乐观锁是什么?
Mutex是悲观锁。
- 乐观锁:乐观锁认为对同一数据的并发操作不会总发生,属于小概率事件,不用每次都对数据上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。不采用数据库自身的锁机制,而是通过程序来实现。
- 悲观锁:对数据被其他事务的修改持保守态度,会通过数据库自身的锁机制来实现,从而保证数据操作的排它性。每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会 阻塞 直到它拿到锁
23. Mutex 有几种模式?
- 正常模式
正常模式下,所有等待锁的 goroutine 按照 FIFO(先进先出)顺序等待。唤醒 的 goroutine 不会直接拥有锁,而是会和新请求 goroutine 竞争锁(还会和处于自旋状态的goroutine竞争 - 这里面的自旋状态的无非就是某次新请求没有竞争成功,进入了自旋状态)。新请求的 goroutine 更容易抢占(因为它正在 CPU 上执行),所以刚刚唤醒的 goroutine有很大可能在锁竞争中失败。在这种情况下,这个被唤醒的 goroutine 会加入到等待队列的前面。不过加入之前会判断自上次阻塞到本次阻塞经过了多长时间,如果超过 1ms 的话,会将 Mutex 标记为饥饿模式。 - 饥饿模式
饥饿模式下,直接由 unlock 把锁交给等待队列中排在第一位的 goroutine (队头),同时,饥饿模式下,新进来的 goroutine 不会参与抢锁也不会进入自旋状态,会直接进入等待队列的尾部。这样很好的解决了老的 goroutine一直抢不到锁的场景。
饥饿模式的触发条件:当一个 goroutine 等待锁时间超过 1 毫秒时,或者当前 队列只剩下一个 goroutine 的时候,Mutex 切换到饥饿模式。
自旋状态:一个goroutine认为一个事件即将发生,它不会进入睡眠状态,自己一直在做空循环,一直等待事件的发生。
25. 怎么控制并发数
- 使用goroutine和channel。可以使用goroutine和channel来控制并发数,例如创建一个缓冲大小为N的通道,表示最多只能有N个goroutine同时执行。在每个goroutine开始执行时,将一个空结构体(或其他无意义的数据)发送到通道中,如果通道已满,则当前goroutine会被阻塞,直到有一个goroutine执行完成并从通道中接收了一个数据。在每个goroutine执行完成后,从通道中接收一个数据,以便其他goroutine可以执行。
func main() {
count := 10 // 最大支持并发
sum := 100 // 任务总数
wg := sync.WaitGroup{} // 控制主协程等待所有子协程执行完之后再退出 (在该程序中是为了让所有的任务(100个)都执行完)
c := make(chan struct{}, count) // 控制任务并发的chan
defer close(c)
for i:=0; i<sum;i++{
wg.Add(1)
c <- struct{}{} // 在通道中满时候,会阻塞在此
go func(j int) {
defer wg.Done()
fmt.Println(j)
<- c // 执行完毕,该goroutine对应的空结构体也被流出
}(i)
}
wg.Wait()
}
- 使用ants
func main() {
pool, err := ants.NewPool(3) // 创建一个大小为 3 的协程池
if err != nil {
panic(err)
}
defer pool.Release()
wg := sync.WaitGroup{} // 定义一个等待组,用于等待所有任务完成
for i := 0; i < 10; i++ {
wg.Add(1)
err := pool.Submit(func() { // 使用 submit 方法来提交任务到协程池中
fmt.Printf("Task %d is done.
", i)
wg.Done()
})
if err != nil {
panic(err)
}
}
wg.Wait()
}
26. 用defer捕获异常
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("捕获到异常:", err)
}
}()
// 产生异常
panic("出现了一个异常")
}
recover() 函数只能在 defer 语句中使用,否则会导致编译错误。recover() 函数的返回值是 panic() 函数传递的参数。
27. 设计一个优雅的线程池
type GoroutinePool struct {
minGoroutines int // 最小协程数量
maxGoroutines int // 最大协程数量
queue chan func() // 任务队列
wg sync.WaitGroup // 等待组(等待任务队列中被各协程完成)
}
func NewGoroutinePool(minGoroutines, maxGoroutines int) *GoroutinePool {
return &GoroutinePool{
minGoroutines: minGoroutines,
maxGoroutines: maxGoroutines,
queue: make(chan func(), 100),
}
}
func (p *GoroutinePool) Start() {
for i := 0; i < p.minGoroutines; i++ {
go p.worker()
}
go p.adjust()
}
func (p *GoroutinePool) worker() {
for task := range p.queue {
task()
p.wg.Done()
}
}
func (p *GoroutinePool) Submit(task func()) {
p.wg.Add(1)
p.queue <- task
}
func (p *GoroutinePool) Wait() {
p.wg.Wait()
}
func (p *GoroutinePool) adjust() {
for {
time.Sleep(time.Second)
select {
case <-p.queue:
if p.minGoroutines < p.maxGoroutines && len(p.queue) > p.minGoroutines {
p.minGoroutines++
}
default:
if len(p.queue) < p.minGoroutines && p.minGoroutines > 1 {
p.minGoroutines--
}
}
go p.worker()
}
}
func main() {
pool := NewGoroutinePool(2, 5)
pool.Start()
for i := 0; i < 6; i++ {
pool.Submit(func() {
fmt.Println(1)
})
}
pool.Wait()
}
28. Channel 分配在栈上还是堆上?哪些对象分配在堆上,哪些对象分配在栈上?
在 Golang 中,Channel 是分配在堆上的,因为 Channel 的作用域和生命周期不仅仅限于某个函数内部,而且需要在协程间传递数据,因此需要在堆上分配空间。
至于哪些对象分配在堆上,哪些对象分配在栈上,这取决于编译器和逃逸分析的结果。一般来说,函数的局部变量会被分配在栈上,而全局变量和动态分配的对象(如使用 new 或 make 函数创建的对象)会被分配在堆上。但是,如果编译器可以确定一个局部变量在函数返回后不再被引用,那么它就可以将该变量分配在栈上,而不是堆上,以提高程序的效率。逃逸分析是一种静态分析技术,可以帮助编译器确定变量的作用域和生命周期,从而优化变量的分配方式。
29. a := 1,a在堆上,还是栈上?
在 Golang 中,基本类型的变量(如 int、float、bool 等)和小的结构体变量通常会被分配在栈上。因此,当你声明一个变量 a := 1 时,a 变量会被分配在栈上。
注意:如果变量 a 被传递给一个函数,或者被存储到一个堆数据结构中,那么它可能会被分配到堆上。这是因为编译器会进行逃逸分析,如果发现变量 a 的生命周期超出了当前函数的作用域,那么它就会被分配到堆上,以确保它在函数返回后仍然可以被访问。
30. 请简述 Go 是如何分配内存的?
- Go的内存分配原则:
Go在程序启动的时候,会先向操作系统申请一块内存(注意这时还只是一段虚拟的地址空间,并不会真正地分配内存),切成小块后自己进行管理。申请到的内存块被分配了三个区域:
spans: spans区域存放mspan(是一些arena分割的页组合起来的内存管理基本单元)的指针
bitmap:bitmap区域标识arena区域哪些地址保存了对象
arena: arena区域就是我们所谓的堆区,Go动态分配的内存都是在这个区域,它把内存分割成8KB大小的页,一些页组合起来称为mspan。
(1)小于32Kb的内存分配:GPM调度模型中,每个M绑定一个P,每个P绑定一个mcache ,当该P管理的本地队列中的某个G想要申请内存时候,会从mcache中申请mspan。如果没有空闲的mspan或者没有特定大小的mspan了,则mcache就会向mcentral中获取。mcentral被所有线程共享。当 mcentral 没有空闲的 mspan 时,会向 mheap 申请。而 mheap 没有资源时,会向操作系统申请新内存。
(2)大于32kb内存分配:对于那些超过32KB的内存申请,会直接从堆上(mheap)上分配对应的数量的内存页(每页大小是8KB)。
(3)内存对齐:Go 语言的内存分配器会将分配的内存按照一定的规则进行对齐。
(4)内存复用:Go 语言的垃圾回收器会尽可能地复用已经分配的内存,以减少内存分配和回收的开销。当某个 mspan 中的对象被回收后,该 mspan 中的空闲对象数量会增加,当空闲对象数量达到一定阈值时,该 mspan 就会被移出空闲列表,以便下次分配内存时可以重复利用。
以上主旨:一般小对象通过 mspan 分配内存;大对象则直接由 mheap 分配内存。
31. 知道 golang 的内存逃逸吗?什么情况下会发生内存逃逸?
- 内存逃逸是什么:本该分配到栈上的变量,跑到了堆上,这就导致了内存逃逸
- 哪几种情况下有内存逃逸:
(1)方法内返回局部变量指针
(2)向 channel 发送指针数据
(3)在闭包中引用包外的值
(4)slice 由于 append 操作超出其容量
(5)slices 中的值是指针的指针或包含指针字段
(6)调用接口类型的方法(接口类型的方法调用是动态调度 - 实际使用的具体实现只能在运行时确定) - 怎么避免内存逃逸?
(1)尽量避免调用接口类型的方法
(2)避免使用函数中指针类型的局部变量 - 逃逸分析
(1)查看:通过go build -gcflags '-m’命令来观察变量逃逸情况
(2)注意:Go编译器会在编译期对考察变量的作用域,就可能会出现内存逃逸。 - 内存逃逸危害
(1)变量在堆上的分配和回收都比在栈上开销大的多。对于 go 这种带 GC 的语言来说,会增加 gc 压力,同时也容易造成内存碎片。
32. 谈谈内存泄露,什么情况下内存会泄露?怎么定位排查内存泄漏问题?
- 内存泄漏是什么:go 中的内存泄漏一般都是 goroutine 泄漏,就是 goroutine 没有被关闭,或者没有添加超时控制,让 goroutine 一只处于阻塞状态,不能被 GC。
- 哪几种情况下有内存泄漏
(1) goroutine 在执行时被阻塞而无法退出
(2)互斥锁未释放或者造成死锁会造成内存泄漏
(3)time.Ticker 是每隔指定的时间就会向通道内写数据。不调用stop()方法
(4)字符串的截取引发的内存泄漏
(5)切片截取引起子切片内存泄漏
(6)函数数组传参引发内存泄漏(参数内存很大) - 排查方式:
(1)一般通过 pprof 是 Go 的性能分析工具
33. GPM调度模型
下面这段话看不懂的话,先学习一下GPM调度模型。视频链接 GPM调度模型 (这个视频不是我的,我觉得他讲的可以)
-
什么是GPM?
G 代表着 goroutine,P 代表着 协程处理器(用于向M提供G队列),M 代表 thread 线程(G的真正执行者),在 GPM 模型,有一个全局队列(Global Queue):存放等待运行的 G,还有一个 P 的本地队列:也是存放等待运行的 G,但数量有限,不超过 256 个。GPM 的调度流程从 go func()开始创建一个 goroutine,新建的 goroutine 优先保存在 P 的本地队列中,如果 P 的本地队列已经满了,则会保存到全局队列中。M 会从 P 的队列中取一个可执行状态的 G 来执行,如果 P 的本地队列为空,会优先从全局队列中取 G,然后再尝试从其它 P 的本地队列中偷取 G。若某个时刻,M 执行某一个 G 时候发生系统调用或者阻塞,会从休眠线程队列中找M,并将与原来的M绑定的P与刚找出的M进行绑定,继续执行原来P的本地队列中的G。若休眠线程中没有M,则该P会被加入空闲P队列。然后原来的M执行的G为非阻塞状态,因为它的P已经走了,所以它没有办法继续执行G,优先会获取原配,然后从空闲P队列中取,否则 该G放入全局队列,该M休眠。 -
注意:
(1)当某个 P 的本地队列中没有 G 时,会优先从全局队列中取 G,然后再尝试从其它 P 的本地队列中偷取 G。(从源码可看到,先从本地队列查询,全局队列查询,然后再从其他P里偷取,具体源码在runtime的proc.go里)
(2)GOMAXPROCS是所有P个数,故有 自旋线程 + 执行线程 <= GOMAPPROCS
(3)自旋线程的概念:全局中没有G了,目前执行的是G0,需要从其它P的本地队列中偷取。
(4)M0、G0的概念:m0是一个进程的第一个线程,负责执行和初始化第一个g(通常是main);g0是在协程切换完成上下文切换。
34. go调度机制中的抢占
因为整个Go程序都是运行在用户态的,所以不能像操作系统那样利用时钟中断来打断运行中的goroutine。也得益于完全在用户态实现,goroutine的调度切换更加轻量。goroutine的调度器也用到了时间片算法,但是和操作系统的线程调度还是有些区别的,主要分为基于协作的抢占式调度和基于信号量的抢占式调度。
- 协作式调度的核心在于G在M上运行的时候主动让出M
- 基于信号量的抢占式调度的本质是G没有主动让出M的时候,强行中断M对G的执行
以下是理解:
- 基于协作的抢占式调度
(1)通过设置环境变量 GODEBUG=asyncpreempt=off启用
(2)对于Go语言中运行时间过长的goroutine,会有一个后台线程持续监控,一旦运行超过10ms,会设置goroutine的协作标识位,资源会被抢占走。 - 基于信号量的抢占式调度
(1)preemptone和preemptM都是用于实现M抢占机制的。preemptone是一个定时器,用于定期检查所有的M是否需要被抢占。如果某个M的运行时间超过了一定的阈值,preemptone会向该M发送抢占信号,让该M主动放弃CPU,以便其他M有机会运行。preemptM是一个函数,它会在需要进行抢占的时候被调用。它会向需要进行抢占的M发送SIGURG信号,以触发该M的抢占操作。
35. 进程、线程、协程有什么区别?
- 进程:是操作系统中资源分配的基本单位,每个进程都有独立的内存空间、代码和数据。进程之间的切换开销较大,因此进程间的并发性较低。
- 线程:是进程中的执行单元,一个进程可以包含多个线程,它们共享进程的资源。线程之间的切换开销较小,因此线程间的并发性较高。
- 协程:是一种用户态的轻量级线程,它不需要操作系统的支持,可以在用户程序中实现。协程通常运行在单个线程中,因此协程的并发性比线程更高。
36. GC
下面这些话看不懂的话,先看GC的讲解视频 GC原理视频,(这个视频不是我的,我觉得他讲的可以)
- GC机制随着golang版本变化如何变化的?
Go V1.3 之前普通标记清除(mark and sweep)方法,整体过程需要启动 STW,效率极低。
GoV1.5 三色标记法,堆空间启动写屏障,栈空间不启动。全部扫描之后,需要重新扫描一次栈(需要 STW),效率普通。
GoV1.8 三色标记法,混合写屏障机制:栈空间不启动(全部标记成黑色),堆空间启用混合写屏障,整个过程不要 STW,效率高。 - 三色标记法:
(1)基本思路:首先将所有对象都放入白色标记表中,然后遍历程序的根节点(只遍历一层),得到灰色节点,然后遍历该灰色节点,将可达的对象,从白色标记为灰色,自身变为黑色。重复上面步骤,知道灰色标记表中无任何对象。
(2)最不希望发生的事(会造成对象无辜的被清理):一个白色对象被黑色对象引用 且 灰色对象与它之间的可达关系的白色对象遭到破坏。
解决方法:强弱三色不变式(强 —— 破坏条件1,即强制性的不允许黑色对象引用白色对象;弱 —— 破坏条件2,黑色对象引用白色对象时,需要满足白色对象存在其它灰色对象对它的引用,或者可以达它的链路上游存在灰色对象),这种强弱不变式就是 屏障机制 - 屏障机制的实现(插入屏障 和 删除屏障)
(1)插入屏障(强三色不变式):(对象被引用时触发的机制)在A对象引用B对象时候,B对象被标记为灰色。(不在栈上使用,因为性能影响大) 缺点:结束时需要STW来重新扫描栈(防止对象丢失,因为黑色对象会创建对象,所以最终会重新扫描一次栈,需要进行短暂的STW)。
(2)删除屏障(弱三色不变式):(对象被删除时触发的机制)被删除的对象,如果自身为灰色或者白色,那么被标记为灰色。缺点:回收精度低。一个对象被删除,此轮会存活。下一轮才会被GC清理掉。
(3)混合写屏障机制(GoV1.8的三色标记法),步骤如下:
1. GC开始将栈上的对象全部扫描并标记为黑色
2. GC期间,任何在栈上创建的新对象,均为黑色
3. 被删除的对象标记为灰色
4. 被添加的对象标记为灰色