文章目录
- go_8gu
-
- slice
- map
- channel
- GMP
-
- 什么是go中的GMP模型
- [什么是go scheduler](#什么是go scheduler)
- go在进行goroutine调度的时候策略是怎么样的呢
- 发生调度的时机有哪些
- m寻找可运行g的过程是什么样的
- gmp能不能去掉p层,去掉了会怎样
- p和m什么时候会被创建
- g0是一个什么样的协程,作用是什么
- g0和用户栈是如何进行切换的
- 内存管理
- gc垃圾回收
go_8gu
本篇文章记录的是我对这些八股的理解,后续会更新正确答案进行对比理解。
slice
slice的底层数据结构
- slice header,里面有3个字段
- 1、指向底层分配数组的一个指针 2、len 3、cap
slice扩容
- slice扩容,会分配新的底层数组,对应的ptr会更新
- 如果是 <256byte 双倍
- 如果是 >256byte 每次按照1.25倍扩容
从一个slice截取出另一个slice,新slice会影响原slice吗
- 新老slice都是共享共一个底层数组
- 修改值的话新slice会影响原slice
- 但如果新slice触发了扩容,可能会分配新的底层slice,就不会对原slice有影响了
slice作为函数参数传递会改变原slice吗
- 虽然函数传递是值拷贝,但由于slice底层数据结构包含指向底层数组的指针,所以对其的修改是会影响到原slice的
- 但如果触发了扩容,可能会分配新的底层数组,slice的ptr会变化,就不会影响到原slice
map
map底层实现原理
- map底层是一个hmap的数据结构,是一个桶数组,里面每一个桶会保存对应hash后高八位,key array,value array,还有溢出桶指针,用于在hash冲突的时候链接到下一个hmap
- 存数据的时候根据key计算hash,按照先后顺序放在对应位置的桶里,如果满了,就申请溢出桶用链接溢出桶指针指向溢出桶
- 取数据的时候也是根据key,计算hash得到要去查找的桶的位置,然后顺序查找桶里的每一项,包括溢出桶,直到找到了为止
map遍历是有序还是无序
- map遍历是无序的,因为每次遍历的时候都会从桶数组的随机位置里面开始遍历
map为什么要设计成遍历无序
- 因为map虽然根据key计算hash得到唯一的桶数组索引,但是由于map会扩容,扩容后数据的先后顺序可能会变化
- 这样设计可以防止程序员对map的数据顺序性有依赖而写出有bug的代码
map怎么实现顺序读取
- map本身遍历是无序的,要实现顺序读取只能固定key的顺序
- 可以先对key进行一定规则的排序,字典序或者hash等,然后根据排序顺序再去读取map即可
map是否支持并发安全
- go中map是不支持并发安全的,如果一时间内有一个协程在写map,当有其他协程读写同一map的时候会panic
- 可以用支持并发安全的concurrentmap,里面用锁实现支持并发安全
map的key必须是可比较的吗,为什么
- map中的key必须是可比较的
- 因为map根据key的hash值后几位决定要存进哪个桶数组中,由于hash冲突的存在,每个桶中会保存相同hash后几位的不同key-value对,需要可比较的key才能区分出那一个是我们所要查找的
map的扩容时机
- map有两种扩容机制,一种是增量扩容,另一种是等量扩容
- 增量扩容在负载因子大于6.5的时候,会触发增量扩容,map容量会扩大为原来的两倍
- 在溢出桶数组长度过长,b>15&&>2b|b<15&&>2b的时候,会触发等量扩容,也是分配一个和原来map相同容量的新map,修改hash函数,保证落桶均匀,然后逐渐把数据迁移过来
- 上述的扩容方式都是渐进式扩容,在每次查询时都会做一些数据迁移,减少一次性迁移带来的性能损耗,直至迁移过程完成
可以对map元素取地址吗
- 不可以,因为map会有扩容机制,每次扩容后元素的地址都可能会变化
- 为了程序员对地址产生依赖,所以map不允许对元素取地址
map删除key会释放其内存吗
- 不会,map删除key,会在其对应桶数组对应高八位位置里置一个empty的标志,标识以后还可以插入
- 如果整个map为空才会释放其map的内存
map可以边遍历边删除吗
- 不可以,可能会有多个协程读,一个协程删了可能会影响其他协程读读取,因为遍历是随机的
channel
什么是csp
- 通信顺序过程:不要用共享内存来实现通信,而是要通过通信来实现共享内存
channel的底层实现原理
- channel的底层数据结构是一个hchan,里面有存放数据的数据缓冲区data,还有队头队尾指针指向读取到的位置和下一次插入的位置,还有生产者和消费者的阻塞队列,同时还有一把锁保证并发读写
向channel发送数据的过程
- 向channel发送数据,首先会查看数据缓冲区是否有空间,有的话优先把数据插入数据缓冲区中
- 如果数据缓冲区中没有空间,则会尝试唤醒一个阻塞的消费者,把数据给它
- 如果也没有阻塞的消费者,则把生产者协程阻塞放入阻塞生产者队列中
向channel读取数据的过程
- 向channel读取数据,首先会查看数据缓冲区中有没有数据,如果有的话直接读取数据
- 如果数据缓冲区没有数据,则检查是否有阻塞的生产者,有的话直接唤醒一个阻塞的生产者,把数据直接给它
- 如果都没有则把消费者进程挂起放到消费者阻塞队列里面
从一个已关闭的channel仍能读出数据吗
- 可以的,已关闭的channel如果里面仍有数据,仍可以从里面读出数据,但是读完后就不可以了,channel会被销毁
channel在什么情况下会发生内存泄漏
- 当channel关闭后,未通知依赖channel的协程,协程未退出仍在等待channel
- 只有消费者 / 只有生产者,导致channel空 / 满了,阻塞队列太多挂起协程
关闭channel会产生异常吗
- channel不可以被重复关闭,如果尝试关闭一个已关闭的channel,会panic
往一个关闭的channel写入数据会发生什么
- 已经关闭的channel不允许写入数据,会产生panic
什么是select
- select是channel的一种非阻塞写法,可以同时监控多个channel里面是否有就绪的数据,有的话优先处理该channel中的数据,保证程序不会被阻塞
select的执行机制是怎样的
- select会同时监听多个channel的情况,一般情况下如果channel都没就绪数据,select会跳转到default,保证程序不被阻塞,如果某个channel有数据就绪可以被处理了,select会跳转到该channel,优先处理该channel的数据,如果同时有多个channel有就绪数据,select会优先选择一个channel进行处理
select的实现原理是怎样的
- 我理解是类似linux中的IO多路复用,epoll函数那样监听多个文件描述符,有就绪的就优先处理
GMP
什么是go中的GMP模型
- go中的GMP模型是指go的调度模型,其中m代表实际运行的操作系统内核线程,g代表goroutines,是轻量级go协程,而p是一个调度器,负责持有g运行的环境,维持一组可运行g,并负责把这组g运行到某个m上,通过这样设计,减少了g和m的直接绑定,减少了锁竞争,同时有handoff机制和窃取机制,能保证每个p不会饥饿,每个m都能有g运行,通过这种m:n的关系提高了性能
什么是go scheduler
- go的调度器,在每个m中都有一个g0协程,不负责执行具体的调度任务,但是负责g的切换和调度
go在进行goroutine调度的时候策略是怎么样的呢
- 以前的版本是非抢占式调度
- 后来go版本改为时间片的抢占调度算法
发生调度的时机有哪些
- 当前g执行完
- 当前g阻塞挂起
- 当前g申请系统调用
- 时间片到被抢占
m寻找可运行g的过程是什么样的
- go在调度goroutine的时候,会优先调用当前p中的g
- 如果当前p中没有g,会申请全局队列的锁,取全局队列中获取一部分g到p中
- 如果全局队列也没有,会从其他p中窃取一些p过来,从而确保没有p或者m饥饿
gmp能不能去掉p层,去掉了会怎样
- gmp不能去掉p层,p层保证了m寻找可执行g的时候,现有一个本地维护的可执行g队列,而不用都去争抢全局g队列产生大量的锁竞争
- 如果去掉了p,每个m寻找g的时候都会去全局队列寻找g,从而都需要争抢全局队列的锁,导致性能低
p和m什么时候会被创建
- go运行时中管理着一组闲置的m和p,当系统中有可运行的m空闲且已经没有p和其绑定时,系统会创建新的p和其绑定,保证其不空闲,同样的系统中待运行的p超出阈值且已经没有m和其绑定时,系统会创建新的m和其绑定,保证其不空闲
g0是一个什么样的协程,作用是什么
- g0是每个m中的特殊g,不和其她g一样负责运行用户函数,而是负责m中g和其他g的调度,当一个g运行完或者是被抢占时,g0负责调度下一个可运行的g在这个m上面运行
g0和用户栈是如何进行切换的
- g0首先会保存当前进程的上下文信息还有栈地址,然后会恢复上下文的信息为下一个将调用的g的信息,最后修改栈地址指针开始运行下一个g
内存管理
go中是如何分配内存的
- go中内存管理模型主要是一个金字塔模型,最底下的是mheap负责管理操作系统直接分配给go运行时的内存,mheap之上是粒度依次变细的内存管理结构,mcenter负责管理一组可分配的不同大小的mcache,每个mcache都包含了mspan,在gmp模型的每一个p中都会管理着一组mcenter,用来给g分配运行所需的内存,go中的内存管理模型就是这样把内存分配为一系列细粒度的管理结构配合gmp达到高性能
go中的内存逃逸是什么,什么时候会发生内存逃逸
- go中的内存逃逸是指分配在栈上的对象由于后续go分析器分析其后续可能还会用到,逃逸到了堆上,由于栈是程序运行结束自动销毁的,所以不需要gc回收,但是堆上的对象不会自动销毁,所以需要gc垃圾回收,太多的内存逃逸会导致gc需要回收的对象太多,影响性能
- 内存逃逸一般发生在局部变量地址作为参数传递给了其他函数,由于其他函数需要这个局部变量,但是局部变量在其函数结束后就会被销毁。go判断这个变量后续可能会被用到,为了函数不出错,就会把这个变量给放在了堆内存里
channel是分配在栈上还是分配在堆上
- channel是分配在堆上的,因为channel负责这协程之间的通信,如果分配在栈上随着函数调用栈道销毁channel就会被销毁,会直接影响协程的通信
go什么情况下会发生内存泄漏
- go内存泄漏一般出现有几种情况,内存泄漏指的是go的对象一直没有被回收导致内存占用不断升高,最终可能会导致oom
- channel关闭时没通知下游go协程退出,一直等待关闭的channel里的数据
- channel用完没有及时关闭,一直在堆上
- go协程中创建了新的go协程,但是没写好退出的逻辑,导致新go协程又会创建新的不断创建下去
go内存泄漏怎么定位和优化
- top 看系统内存占用
- pprof,查看内存泄漏具体情况
- 确保代码中有正确的退出逻辑,同时可以增加gc的扫描频率,确保没用到的对象更及时地被清理
gc垃圾回收
常见的gc回收方式有哪些
- 标记清除算法,stw扫描,标记出哪些是垃圾,然后清除再恢复stw
go中的gc使用的是什么算法
- 并发三色标记+混合写屏障算法
go中的并发三色标记是什么
- go中,三色指的是黑色、灰色和白色,黑色指的是已经扫描过而且所引用的对象也已经扫描过了的对象,灰色指的是已经被扫描过,但是所引用的对象还没被扫描,而白色指的是这个对象还没有被扫描。