Questions - underlying
1. init() 函数执行时机
Golang 包的初始化顺序:
- 解析依赖(import),导入其他包
- 初始化常量(const)
- 初始化全局变量(var)
- 执行初始化函数(init),若包中包含多个
init
函数,则顺序未知 - 执行
main
函数
package main
import "fmt"
func init() {
fmt.Println("init1:", a)
}
func init() {
fmt.Println("init2:", a)
}
var a = 10
const b = 100
func main() {
fmt.Println("main:", a)
}
// 执行结果
// init1: 10
// init2: 10
// main: 10
2. 局部变量内存分配在栈上还是堆上
需要根据编译器的内存逃逸分析(escape analysis)决定。
- 未发生逃逸,则分配在栈上
- 发生逃逸,则分配在堆上
func foo() *int {
v := 11
return &v
}
func main() {
m := foo()
println(*m) // 11
}
局部变量v
,由于函数返回其指针,导致其作用域超出当前函数,所以需要分配在堆上。
3. 空接口类型可以比较么
空接口类型(interface{})变量可以使用==
,!=
进行比较,在两种情况下相等:
- 若均为
nil
- 底层类型相同,并且值相同
func main() {
type stu struct {
name string
age int
}
var s1, s2 any = &stu{name: "Alice"}, &stu{name: "Alice"}
var s3, s4 any = stu{name: "Alice"}, stu{name: "Alice"}
fmt.Println(s1 == s2) // false
fmt.Println(s3 == s4) // true
}
s1
,s2
类型相同但值不同s3
,s4
类型相同且值相同
4. 两个 nil 可能不相等么
可能
当值为 nil 的变量转换成接口类型时,接口类型包含变量类型和其值 nil。
而值为 nil 的接口变量,其底层类型和值均为 nil 。
此时比较两者将被认定为不相等。
func main() {
var p *int = nil
var i interface{} = p
fmt.Println(i == p) // true
fmt.Println(p == nil) // true
fmt.Println(i == nil) // false
}
5. 简述 GC 的原理
Golang 的 GC 采用:
- 三色标记法
- 混合写屏障
GC 处理包含两个阶段:
- 标记:从根对象触发标记所有的存活对象
- 清除:回收未被标记的对象
三色标记法
对象分为三类:
- 黑色:活跃对象
- 灰色:待扫描对象
- 白色:待回收的对象
标记流程:
- 将根对象标记为灰色,加入灰色对象集合
- 从灰色对象集合取出对象,标记为 黑色
- 将黑色对象指向的对象标记为灰色
- 继续步骤 2)直到灰色对象不存在为止
写屏障
三色不变性
- 强三色不变性:黑色对象只会指向灰色/黑色对象,不能指向白色对象
- 弱三色不变性:黑色对象指向的白色对象,必须包含一条从灰色对象经由多个白色对象的可达路径
插入写屏障
由 Dijkstra 提出,若新增了指向对象的指针:
- 若对象为白色,则标记为灰色
- 若为其他标记则不变
满足强三色不变性。
删除写屏障
由 Yuasa 提出,删除写屏障会保证开启写屏障时堆上所有对象的可达,所以也被称作快照垃圾收集(Snapshot GC)。
在对象的引用被删除时:
- 若对象颜色为白色,则标记为灰色
- 其他标记则不改变
满足弱三色不变性
混合写屏障
同时使用插入和删除写屏障:
- 对象新增指向其引用时,若为白色则标记成灰色
- 对象引用被删除,若为白色则标记成灰色
- 所有新创建的对象标记为黑色
- 防止新对象误回收
- 无需重新扫描栈空间
完整流程
- 标记准备(Mark Setup),需要 STW,开启写屏障
- 使用三色标记法标记(Marking),并发执行
- 标记结束(Mark Termination),需要 STW,关闭写屏障
- 清理(Sweeping),并发执行
6. 函数返回局部变量的指针是否安全
安全
编译器会进行逃逸分析,此时的局部变量会分配在堆上。
7. 非接口类型的变量一定能狗调用指针方法集么
不一定
仅有变量是可以寻址的情况下能够调用。
以下类型的变量不可寻址:
- 字符串中的字节
- map 中的元素
- 常量
- 包级别的函数
type T string
func (t *T) hello() {
fmt.Println("hello")
}
func main() {
var t1 T = "ABC"
t1.hello() // hello
const t2 T = "ABC"
t2.hello() // error: cannot call pointer method on t
}
8. 简述变量在转换成接口类型后可调用方法集的情况
对于变量的类型和其接口方法的接收者类型,当其转换成接口后的方法调用情况如下:
变量类型\接口方法接收者类型 | 值 | 指针 |
---|---|---|
值 | 可 | 不可 |
指针 | 可 | 可 |
对于变量的类型:
- 指针类型,转换成接口后,接口变量持有的是原变量的指针
- 可直接调用接收者为指针类型的接口方法
- 可间接调用接收者为值类型(自动解引用)的接口方法
- 值类型,转换成接口后,接口变量持有的是原变量的拷贝
- 可直接调用接收者为值类型的接口方法
- 不可调用接收者为指针类型的接口方法,无法获取原变量的指针,即接口持有的值类型变量无法寻址
9. 简述 Golang 内存管理
Golang 内存分配器包含:
- 内存管理单元:
runtime.mspan
,内存管理单元构成双向链表 内存管理单元以 页(8KB,不是操作系统的内存页) 向堆申请内存。 - 线程缓存:
runtime.mcache
,与 P 绑定,用于缓存程序申请的微小对象。 - 中心缓存:
runtime.mcentral
,访问中心缓存需要互斥锁 - 页堆:
runtime.mheap
,管理堆内存
内存分配
- 微对象:小于 16 字节的对象;会使用线程缓存上的微分配器提高微对象分配的性能
- 小对象:16 字节到 32,768 字节的对象;从线程缓存、中心缓存或者堆中获取内存管理单元并从内存管理单元找到空闲的内存空间;
- 大对象:大于 32KB 的大对象会单独处理;不会从线程缓存或者中心缓存中获取内存管理单元,而是直接分配大内存
内存分配顺序:
- 从线程缓存获取
- 若失败,则从中心缓存获取
- 若失败,则从页堆(mheap)中获取
10. 简述 GMP 模型
- G:Goroutine
- M:Machine,内核态线程
- P:Processor,调度器
GMP 流程
- 创建一个 G
- 加入队列:
- 优先加入 P 的本地队列;
- 本地队列已满则加入全局队列;
- M 和 P 一对一绑定,M 从 P 的本地队列获取 G 运行:
- 若本地队列为空,则从全局队列获取;
- 若全局队列和本地队列均为空,则从其他的 MP 组合中偷取一半数量的 G 来运行;
- M 执行 G 若发生阻塞,则当前的 M 和 P 会解绑 (detach),然后创建 或 唤醒 一个 M 与 P 绑定。
自旋
当 MP 组合无法获取 G 执行时,M 将进入自旋状态。
休眠
当 M 一段时间内没有获取 P 与之绑定时,M 将进入休眠状态。
11. Goroutine 何时发生阻塞
- channel阻塞:当goroutine读写channel发生阻塞时,会调用gopark函数,该G脱离当前的M和P,调度器将新的G放入当前M。
- 系统调用:当某个G由于系统调用陷入内核态,该P就会脱离当前M,此时P会更新自己的状态为Psyscall,M与G相互绑定,进行系统调用。结束以后,若该P状态还是Psyscall,则直接关联该M和G,否则使用闲置的处理器处理该G。
- 系统监控:当某个G在P上运行的时间超过10ms时候,或者P处于Psyscall状态过长等情况就会调用retake函数,触发新的调度。
- 主动让出:由于是协作式调度,该G会主动让出当前的P(通过GoSched),更新状态为Grunnable,该P会调度队列中的G运行。
12. GMP 有哪些状态
G 的状态:
_Gidle:刚刚被分配并且还没有被初始化,值为0,为创建goroutine后的默认值
_Grunnable: 没有执行代码,没有栈的所有权,存储在运行队列中,可能在某个P的本地队列或全局队列中(如上图)。
_Grunning: 正在执行代码的goroutine,拥有栈的所有权(如上图)。
_Gsyscall:正在执行系统调用,拥有栈的所有权,与P脱离,但是与某个M绑定,会在调用结束后被分配到运行队列(如上图)。
_Gwaiting:被阻塞的goroutine,阻塞在某个channel的发送或者接收队列(如上图)。
_Gdead: 当前goroutine未被使用,没有执行代码,可能有分配的栈,分布在空闲列表gFree,可能是一个刚刚初始化的goroutine,也可能是执行了goexit退出的goroutine(如上图)。
_Gcopystac:栈正在被拷贝,没有执行代码,不在运行队列上,执行权在
_Gscan : GC 正在扫描栈空间,没有执行代码,可以与其他状态同时存在。
P的状态:
_Pidle :处理器没有运行用户代码或者调度器,被空闲队列或者改变其状态的结构持有,运行队列为空
_Prunning :被线程 M 持有,并且正在执行用户代码或者调度器(如上图)
_Psyscall:没有执行用户代码,当前线程陷入系统调用(如上图)
_Pgcstop :被线程 M 持有,当前处理器由于垃圾回收被停止
_Pdead :当前处理器已经不被使用
M 状态:
自旋线程:处于运行状态但是没有可执行goroutine的线程,数量最多为GOMAXPROC,若是数量大于GOMAXPROC就会进入休眠。
非自旋线程:处于运行状态有可执行goroutine的线程。
13. GMP 中 P 的作用
- 每个 P 有自己的本地队列,大幅度的减轻了对全局队列的直接依赖,所带来的效果就是锁竞争的减少。而 GM 模型的性能开销大头就是锁竞争。
- 每个 P 相对的平衡上,在 GMP 模型中也实现了 Work Stealing 算法,如果 P 的本地队列为空,则会从全局队列或其他 P 的本地队列中窃取可运行的 G 来运行,减少空转,提高了资源利用率。
14. 什么是 work stealing
当某个 goroutine 一直占用资源 ,那么GMP模型会从正常模式转变为饥饿模式,允许其它goroutine使用work stealing抢占。
work stealing算法指,一个线程如果处于空闲状态,则帮其它正在忙的线程分担压力,从全局队列取一个G任务来执行,可以极大提高执行效率。