1.Go语言中的nil
零值是Go语言中变量在声明之后但是未初始化被赋予的该类型的一个默认值。
在Go语言中,布尔类型的零值(初始值)为 false,数值类型的零值为 0,字符串类型的零值为空字符串""
,而指针、切片、映射、通道、函数和接口的零值则是 nil。
注意:Go中的两个nil可能是不相等的,因为接口(interface) 是对非接口值(例如指针,struct等)的封装,内部实现包含 2 个字段,类型 T 和 值 V。一个接口等于 nil,当且仅当 T 和 V 处于 unset 状态(T=nil,V is unset)。两个接口值比较时,会先比较 T,再比较 V。 接口值与非接口值比较时,会先将非接口值尝试转换为接口值,再比较。
2.Go语言中空struct{}的作用
使用空结构体可以节省内存,一般作为占位符使用,表明这里不需要一个值:(1)通常使用map表示集合时,value可以使用struct{}作为占位符,(2)还有使用channel控制并发时,只是需要一个信号,并不需要传递值,也适用struct{}代替。
(1)golang中Slice,Map,Function等数据类型是不可以比较的,因此struct可以比较也不可以比较,主要看结构体内的成员变量是否都可以比较;结论就是同一个struct的两个实例可比较也不可比较,当结构不包含不可直接比较成员变量时可直接比较,否则不可直接比较。
(2)struct必须是可比较的,才能作为map的key,否则编译时报错。
(3)GO中map、slice、func是不可比较类型,可比较:int、ifloat、string、bool、complex、pointe、channel、interface、array
3.Go语言tag的用处
tag 可以理解为 struct 字段的注解,可以用来定义字段的一个或多个属性。框架/工具可以通过反射获取到某个字段定义的属性,采取相应的处理方式。
package main
import "fmt"
import "encoding/json"
type Stu struct {
Name string `json:"stu_name"`
ID string `json:"stu_id"`
Age int `json:"-"`
}
func main() {
buf, _ := json.Marshal(Stu{"Tom", "t001", 18})
fmt.Printf("%s\n", buf)
}
该例子使用 tag 定义了结构体字段与 json 字段的转换关系,Name -> stu_name
, ID -> stu_id
,忽略 Age 字段。很方便地实现了 Go 结构体与不同规范的 json 文本之间的转换。
4.Go中的defer
defer意为延迟,在go lang中用于延迟执行一个函数,主要用于帮助我们处理资源释放、连接关闭等操作。
每个defer语句都对应一个_defer实例,多个实例使用指针连接起来形成一个单连表,保存在gotoutine数据结构中,每次插入_defer实例,均插入到链表的头部,函数结束再一次从头部取出,从而形成后进先出的效果。
- 多个 defer 语句,遵从后进先出(Last In First Out,LIFO)的原则,最后声明的 defer 语句,最先得到执行。
- defer 在 return 语句之后执行,但在函数退出之前,defer 可以修改返回值。( Go 的返回机制是执行 return 语句后,Go 会创建一个临时变量保存返回值,因此对有名返回值无效)
defer与panic和recover结合,形成了Go语言风格的异常与捕获机制。
defer func() {
if err := recover(); err != nil {
// 打印异常,关闭资源,退出此函数
fmt.Println(err)
}
}()
5.Golang中的context
总结:context的主要功能就是用于控制协程退出和附加链路信息。核心实现的结构体有4个,最复杂的是cancelCtx,最常用的是cancelCtx和valueCtx。整体呈树状结构,父子节点间同步取消信号。
接下来介绍Context的基础用法,最为重要的就是3个基础能力,取消、超时、附加值
5.1新建一个context
ctx := context.TODO()
ctx := context.Background()
这两个方法返回的内容是一样的,都是返回一个空的Context,这个Context一般用来做父Context
5.2WithCancel
// 函数声明
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
// 用法:返回一个子Context和主动取消函数
ctx, cancel := context.WithCancel(parentCtx)
根据传入的Context生成一个子Context和一个取消函数。当父Context有相关取消操作,或者直接调用cancel函数的话,子Context就会被取消。
5.3WithTimeout
// 函数声明
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
// 用法:返回一个子Context(会在一段时间后自动取消),主动取消函数
ctx := context.WithTimeout(parentCtx, 5*time.Second)
例如检查:
select {
// 轮询检测是否已经超时
case <-ctx.Done():
这个函数在日常工作中使用得非常多,简单来说就是给Context附加一个超时控制,当超时ctx.Done()返回的channel就能读取到值,协程可以通过这个方式来判断执行时间是否满足要求
5.4 WithDeadline
// 函数声明
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
// 用法:返回一个子Context(会在指定的时间自动取消),主动取消函数
ctx, cancel := context.WithDeadline(parentCtx, time.Now().Add(5*time.Second))
这个函数感觉用得比较少,和WithTimeout相比的话就是使用的是截止时间
5.5WithValue
// 函数声明
func WithValue(parent Context, key, val interface{}) Context
// 用法: 传入父Context和(key, value),相当于存一个kv
ctx := context.WithValue(parentCtx, "name", 123)
// 用法:将key对应的值取出
v := ctx.Value("name")
这个函数常用来保存一些链路追踪信息,比如API服务里会有来保存一些来源ip、请求参数等
6.Go中new和make的区别
答:make和new都是golang用来分配内存的內建函数,且在堆上分配内存,make 即分配内存,也初始化内存。new只是将内存清零,并没有初始化内存。make是用于引用类型(map,chan,slice)的创建,返回引用类型的本身,new创建的是指针类型,new可以分配任意类型的数据,返回的是指针。
变量初始化,一般包括2步,变量声明 + 变量内存分配,var关键字就是用来声明变量的,new和make函数主要是用来分配内存的并且在堆上分配内存;
var声明值类型的变量时,系统会默认为他分配内存空间,并赋该类型的零值:比如布尔、数字、字符串、结构体;
如果指针类型或者引用类型的变量,系统不会为它分配内存,默认就是nil
。此时如果你想直接使用,那么系统会抛异常,必须进行内存分配后,才能使用。
new 和 make 两个内置函数,主要用来分配内存空间,有了内存,变量就能使用了,主要有以下2点区别:
使用场景区别:
make 只能用来分配及初始化类型为slice、map、chan 的数据,并且返回引用类型本身。
new 可以分配任意类型的数据,返回的是指针类型;只是将内存清零,并不初始化内存。
返回值区别:
这3种类型是引用类型,就没有必要返回他们的指针
func make(t Type, size ...IntegerType) Type
new函数原型如下,返回一个指向该类型内存地址的指针
func new(Type) *Type
7.Go中函数的参数传递
Go中的函数参数传递都是值传递,所谓值传递即在调用函数时将实际参数复制一份到函数中,函数内对参数修改并不会影响到实际参数;所谓引用传递指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数做的修改将影响到实际参数。
参数如果是非引用类型(int、string、struct等这些),这样就在函数中就无法修改原内容数据;如果是引用类型(指针、map、slice、chan等这些),这样就可以修改原内容数据。
什么是引用类型:即别名,声明引用并不开辟内存单元
8.Go中的slice
切片是基于数组实现的,它的底层是数组,可以理解为对底层数组的抽象。
type slice struct {
array unsafe.Pointer //指向底层数组的指针,占用8个字节
len int //切片的长度,占用8个字节
cap int //切片的容量,cap 总是大于等于 len 的,占用8个字节
}
对于append向slice添加元素时,假如slice容量够用,则追加新元素进去,slice.len++,返回原来的slice。当原容量不够,则slice先扩容,扩容之后slice得到新的slice,将元素追加进新的slice,slice.len++,返回新的slice。对于切片的扩容规则:当切片比较小时(容量小于1024),则采用较大的扩容倍速进行扩容(2倍),避免频繁扩容,从而减少内存分配的次数和数据拷贝的代价。当切片较大的时(原来的slice的容量大于或者等于1024),采用较小的扩容倍速(1.25倍),主要避免空间浪费,网上其实很多总结的是1.25倍,那是在不考虑内存对齐的情况下,实际上还要考虑内存对齐,扩容是大于或者等于1.25倍。
9.Go中的map
9.1map的底层实现
golang中map的底层实现主要是哈希表,结构体有两个:hmap和bmap.
type hmap struct {
count int //元素个数,调用len(map)时直接返回
flags uint8 //标志map当前状态,正在删除元素、添加元素.....
B uint8 //单元(buckets)的对数 B=5表示能容纳32个元素
noverflow uint16 //单元(buckets)溢出数量,如果一个单元能存8个key,此时存储了9个,溢出了,就需要再增加一个单元
hash0 uint32 //哈希种子
buckets unsafe.Pointer //指向单元(buckets)数组,大小为2^B,可以为nil
oldbuckets unsafe.Pointer //扩容的时候,buckets长度会是oldbuckets的两倍
nevacute uintptr //指示扩容进度,小于此buckets迁移完成
extra *mapextra //与gc相关 可选字段
}
buckets:一个指针,指向一个bmap数组、存储多个桶。
oldbuckets: 是一个指针,指向一个bmap数组,存储多个旧桶,用于扩容。
overflow:overflow是一个指针,指向一个元素个数为2的数组,数组的类型是一个指针,指向一个slice,slice的元素是桶(bmap)的地址,这些桶都是溢出桶。为什么有两个?因为Go map在哈希冲突过多时,会发生扩容操作。[0]表示当前使用的溢出桶集合,[1]是在发生扩容时,保存了旧的溢出桶集合。overflow存在的意义在于防止溢出桶被gc。
type bmap struct {
topbits [8]uint8
keys [8]keytype
values [8]valuetype
pad uintptr
overflow uintptr
}
一个桶(bmap)可以存储8个键值对。如果有第9个键值对被分配到该桶,那就需要再创建一个桶,通过overflow指针将两个桶连接起来。在hmap中,多个bmap桶通过overflow指针相连,组成一个链表。
键和值是分开存放的,原因是当key和value类型不一样的时候,key和value占用字节大小不一样,使用key/value这种形式可能会因为内存对齐导致内存空间浪费,所以Go采用key和value分开存储的设计,更节省内存空间
哈希表的特点是根据哈希函数求得哈希值,来存储对应的key,golang中根据哈希值将其分为高位和低位;
低8位用于寻找当前key属于哪个hmap中的哪个bucket;而高8位用于确定将其存放在桶中(bucket)的哪个位置;
注意:一个桶内最多有8个位置。这也是为什么map
无法使用cap()
来求容量的关键原因:map
的容量是编译器进行计算后得出的一个结果,由于桶的存在,map
在内存中实际存放的大小不一定同make
出来后的map
的大小一致。
9.2map的并发安全性
map默认是并发不安全的,同时对map进行并发读写时,程序会panic,原因如下:
Go 官方在经过了长时间的讨论后,认为 Go map 更应适配典型使用场景(不需要从多个 goroutine 中进行安全访问),而不是为了小部分情况(并发访问),导致大部分程序付出加锁代价(性能),决定了map不支持并发安全。
方式一:使用读写锁 map + sync.RWMutex
var lock sync.RWMutex
方式二:使用Go提供的 sync.Map
var map sync.Map
9.3扩容时机
条件一:超过负载
map元素个数 > 6.5 * 桶个数
条件二:溢出桶太多
当桶总数 < 2 ^ 15 时,如果溢出桶总数 >= 桶总数,则认为溢出桶过多。
当桶总数 >= 2 ^ 15 时,直接与 2 ^ 15 比较,当溢出桶总数 >= 2 ^ 15 时,即认为溢出桶太多了。
9.4 其他问题
问:map 中删除一个 key,它的内存会释放么?
通过delete删除map的key,执行gc不会,内存没有被释放,如果通过map=nil,内存才会释放
问:nil map 和空 map 有何不同?
nil map是未初始化的map,空map是长度为空
10.Go中channel(通道)
**首先说明:**Channel被设计用来实现协程间的通信,其作用域和生命周期不可能仅限于某个函数内部,所以golang直接将其分配在堆上。
使用场景:停止信号监听、定时任务、生产消费解藕、控制并发数
通过var声明或者make函数创建的channel变量是一个存储在函数栈帧上的指针,占用8个字节,指向堆上的hchan结构体
type hchan struct {
qcount uint // chan 里元素数量
dataqsiz uint // chan 底层循环数组的长度
buf unsafe.Pointer // 指向底层循环数组的指针,// 只针对有缓冲的 channel
elemsize uint16 // chan 中元素大小
closed uint32 // chan 是否被关闭的标志
elemtype *_type // chan 中元素类型
sendx uint // 已发送元素在循环数组中的索引
recvx uint // 已接收元素在循环数组中的索引
recvq waitq // 等待接收的 goroutine 队列
sendq waitq // 等待发送的 goroutine 队列
lock mutex // 保护 hchan 中所有字段
}
buf指向一个底层的循环数组,只有设置为有缓存的channel才会有buf
sendx和recvx分别指向底层循环数组的发送和接收元素位置的索引
sendq和recvq分别表示发送数据的被阻塞的goroutine和读取数据的goroutine,这两个都是一个双向链表结构
sendq和recvq 的结构为 waitq 类型,sudog是对goroutine的一种封装
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6sCfFshN-1667188886837)(/Users/bytedance/Desktop/下载.png)]
操作 | nil channel | closed channel | not nil,not channel |
---|---|---|---|
close | panic | panic | 正常关闭 |
写ch<- | 阻塞 | panic | 阻塞或正常写入数据,非缓冲型channel没有等待接收者或缓冲型channel buf满时会被阻塞 |
读<-ch | 阻塞 | 读到对应类型的零值 | 阻塞或正常读取数据,缓冲型channel为空或非缓冲型channel等待发送者时会阻塞 |
向channel写数据的流程:
- 如果等待接收队列recvq不为空,说明缓冲区中没有数据或者没有缓冲区,此时直接从recvq取出G,并把数据写入,最后把该G唤醒,结束发送过程;
- 如果缓冲区中有空余位置,将数据写入缓冲区,结束发送过程;
- 如果缓冲区中没有空余位置,将待发送数据写入G,将当前G加入sendq,进入睡眠,等待被读goroutine唤醒;
向channel读数据的流程:
- 如果等待发送队列sendq不为空,且没有缓冲区,直接从sendq中取出G,把G中数据读出,最后把G唤醒,结束读取过程;
- 如果等待发送队列sendq不为空,此时说明缓冲区已满,从缓冲区中首部读出数据,把G中数据写入缓冲区尾部,把G唤醒,结束读取过程;
- 如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程;将当前goroutine加入recvq,进入睡眠,等待被写goroutine唤醒;
总结hchan结构体的主要组成部分有四个:
- 用来保存goroutine之间传递数据的循环数组:buf
- 用来记录此循环数组当前发送或接收数据的下标值:sendx和recvx
- 用于保存向该chan发送和从该chan接收数据被阻塞的goroutine队列: sendq 和 recvq
- 保证channel写入和读取数据时线程安全的锁:lock
10.1 无缓冲channel和有缓冲channel的区别
chan := make(chan int) // 无缓冲
chan := make(chan int, 1) // 有缓冲
最大的区别是一个是同步的,一个是非同步的
- 无缓冲:例如 chan<-1,不仅是向通道放入1,而是要一直等有别的goroutine接收这个参数,那么 chan<-1 才会继续走下去,否则会一直阻塞着。(即如果在一个协程里执行放入取出操作的话会一直阻塞,需要在不同的协程执行)
- 有缓冲:例如 chan<-1 ,执行后不会阻塞,因为缓冲大小为1;只有当放第二个值且第一个值还没被人拿走,才会阻塞。
11.Go中的select
go的select为golang提供了IO多路复用机制,和其他IO复用一样,用于检测是否有读写事件发生,是否ready。
- 每个 case 都必须是一个通信。
由于 select 语句是专为通道设计的,所以每个 case 表达式中都只能包含操作通道的表达式,并且是单协程操作。 - 如果有多个 case 都可以运行,select 会随机公平地选出一个执行,其他不会执行。
- 如果多个 case 都不能运行,若有 default 子句,则执行该语句,反之,select 将阻塞,直到某个 case 可以运行。
- 所有 channel 表达式都会被求值。
11.1 select 使用不当会发生死锁
如果通道没有数据发送,但 select 中有存在接收通道数据的语句,将发生死锁。
package main
func main() {
ch := make(chan string)
select {
case <-ch:
// 操作
}
}
11.2 select和for结合使用
select 语句只能对其中的每一个case表达式各求值一次。所以,如果想连续或定时地操作其中的通道的话,就需要通过在for语句中嵌入select语句的方式实现。
11.3 select实现超时机制
主要使用的 time.After
实现超时控制。
func main() {
ch := make(chan int)
quit := make(chan bool)
go func() {
for {
select {
case num := <-ch: //如果有数据,下面打印。但是有可能ch一直没数据
fmt.Println("num = ", num)
case <-time.After(3 * time.Second): //上面的ch如果一直没数据会阻塞,那么select也会检测其他case条件,检测到后3秒超时
fmt.Println("超时")
quit <- true //写入
}
}
}()
for i := 0; i < 5; i++ {
ch <- i
time.Sleep(time.Second)
}
<-quit //这里暂时阻塞,直到可读
fmt.Println("程序结束")
}
12.Go中的init函数
- init函数的主要作用:
1)初始化不能采用初始化表达式初始化的变量。
2)程序运行前的注册。
3)实现sync.Once功能。 - init函数的主要特点:
1)init函数先于main函数自动执行,不能被其他函数调用;
2)init函数没有输入参数、返回值;
3)每个包可以有多个init函数;包的每个源文件也可以有多个init函数
4)同一个包的init执行顺序,golang没有明确定义,编程时要注意程序不要依赖这个执行顺序。
5)不同包的init函数按照包导入的依赖关系决定执行顺序。 - golang程序初始化
1)初始化导入的包(包的初始化顺序并不是按导入顺序(“从上到下”)执行的,runtime需要解析包依赖关系,没有依赖的包最先初始化,与变量初始化依赖关系类似,参见golang变量的初始化);
2)初始化包作用域的变量(该作用域的变量的初始化也并非按照“从上到下、从左到右”的顺序,runtime解析变量依赖关系,没有依赖的变量最先初始化,参见golang变量的初始化);
3)执行包的init函数;
13.Go中的内存逃逸分析
在一段程序中,每一个函数都会有自己的内存区域存放自己的局部变量、返回地址等,这些内存会由编译器在栈中进行分配,每一个函数都会分配一个栈桢,在函数运行结束后进行销毁,但是有些变量我们想在函数运行结束后仍然使用它,那么就需要把这个变量在堆上分配,这种从"栈"上逃逸到"堆"上的现象就成为内存逃逸。
在栈上分配的地址,一般由系统申请和释放,不会有额外性能的开销,比如函数的入参、局部变量、返回值等。在堆上分配的内存,如果要回收掉,需要进行 GC,那么GC 一定会带来额外的性能开销。
- 指针逃逸:函数返回值为局部变量的指针,函数虽然退出了,但是因为指针的存在,指向的内存不能随着函数结束而回收,因此只能分配在堆上。
- 栈空间不足:当栈空间足够时,不会发生逃逸,但是当变量过大时,已经完全超过栈空间的大小时,将会发生逃逸到堆上分配内存。
- 变量大小不确定:编译期间无法确定slice的长度,这种情况为了保证内存的安全,编译器也会触发逃逸,在堆上进行分配内存。
- 动态类型:动态类型就是编译期间不确定参数的类型、参数的长度也不确定的情况下就会发生逃逸;空接口 interface{} 可以表示任意的类型,如果函数参数为 interface{},编译期间很难确定其参数的具体类型,也会发生逃逸。
- 闭包引用对象
14.Go中的for range
在for k, v := range nums
遍历中, k 和 v 在内存中只会存在一份,即之后每次循环时遍历到的数据都是以值覆盖的方式赋给 k 和 v,k,v 的内存地址始终不变。由于有这个特性,for循环里面如果开协程,不要直接把 k 或者 v 的地址传给协程。
15.数组和切片的异同点
相同点:
- 都是存储一组相同类型的数据结构
- 都是通过下标来访问,并且有容量长度,长度通过len获取,容量通过cap获取
区别:
- 数组是定长,访问和复制不能超过定义的长度,否则就会下标越界,切片的长度和容量可以自动扩充
- 数组是值类型,切片是引用类型,每个切片都引用了一个底层数组,切片本身不存储数据,都是这底层数组存储数据,所以修改切片的时候修改的是底层数组中的数据;切片一旦扩容,指向一个新的底层数组,内存地址也就随之改变了。
16.Go中的调度器(GMP)
G(Goroutine)
:协程 Goroutine 的缩写,相当于操作系统中的进程控制块。其中存着 goroutine 的运行时栈信息,CPU 的一些寄存器的值以及执行的函数指令等。M(Machine)
:代表一个操作系统的内核线程。一个 M 直接关联一个 os 内核线程,用于执行 G。M 会优先从关联的 P 的本地队列中直接获取待执行的 G。M 保存了 M 自身使用的栈信息、当前正在 M上执行的 G 信息、与之绑定的 P 信息。P(Processor)
:代表了 M 所需的上下文环境,即运行 G 所需要的资源。是处理用户级代码逻辑的处理器,可以将其看作一个局部调度器使 go 代码在一个线程上跑。当 P 有任务时,就需要创建或者唤醒一个系统线程来执行它队列里的任务,所以 P 和 M 是相互绑定的。
-
G
type g struct { stack stack // 描述真实的栈内存,包括上下界 goid int64 // goroutine 的 ID m *m // 当前的 m sched gobuf // goroutine 切换时,用于保存 g 的上下文 param unsafe.Pointer // 用于传递参数,睡眠时其他 goroutine 可以设置 param,唤醒时该goroutine可以获取 waitsince int64 // g 被阻塞的大体时间 lockedm *m // G 被锁定只在这个 m 上运行 }
其中 sched 比较重要,该字段保存了 goroutine 的上下文。goroutine 切换的时候不同于线程有 OS 来负责这部分数据,而是由一个 gobuf 结构体来保存。
-
M
type m struct { g0 *g // 带有调度栈的goroutine gsignal *g // 处理信号的goroutine tls [6]uintptr // thread-local storage curg *g // 当前运行的goroutine p puintptr // 关联p和执行的go代码 nextp puintptr id int32 mallocing int32 // 状态 spinning bool // m是否out of work blocked bool // m是否被阻塞 inwb bool // m是否在执行写屏蔽 ncgocall uint64 // cgo调用的总数 ncgo int32 // 当前cgo调用的数目 alllink *m // 用于链接allm mcache *mcache // 当前m的内存缓存 lockedg *g // 锁定g在当前m上执行,而不会切换到其他m createstack [32]uintptr // thread创建的栈 }
结构体 M 中,有两个重要的字段:
- curg:代表结构体M当前绑定的结构体 G 。
- g0 :是带有调度栈的 goroutine,普通的 goroutine 的栈是在堆上分配的可增长的栈,但是 g0 的栈是 M 对应的线程的栈。与调度相关的代码,会先切换到该 goroutine 的栈中再执行。
-
P
type p struct { lock mutex id int32 status uint32 // 状态,可以为pidle/prunning/... schedtick uint32 // 每调度一次加1 syscalltick uint32 // 每一次系统调用加1 m muintptr // 会链到关联的m goidcache uint64 // goroutine的ID的缓存 // 可运行的goroutine的队列 runqhead uint32 // 本地队列对头 runqtail uint32 // 本地队列队尾 runq [256]guintptr // // 本地队列,大小256的数组,数组往往会被都读入到缓存中,对缓存友好,效率较高 runnext guintptr // 下一个运行的g }
-
Sched:调度器结构,它维护有存储M和G的全局队列,以及调度器的一些状态信息
type schedt struct { ... runq gQueue // 全局队列,链表(长度无限制) runqsize int32 // 全局队列长度 ... }
G | M | P | |
---|---|---|---|
数量限制 | 无限制,受机器内存影响 | 有限制,默认最多10000 | 有限制,最多GOMAXPROCS个(默认CPU核心数) |
创建时机 | go func | 当没有足够的M来关联P并运行其中的可运行的G时会请求创建新的M | 在确定了P的最大数量n后,运行时系统会根据这个数量创建个P |
17.Go中的垃圾回收机制(GC)
Go的GC回收有三次演进过程:
Go V1.3之前普通标记清除(mark and sweep)方法,整体过程需要启动STW,效率极低。
GoV1.5三色标记法,堆空间启动写屏障,栈空间不启动,全部扫描之后,需要重新扫描一次栈(需要STW),效率普通。
GoV1.8三色标记法,混合写屏障机制:栈空间不启动(全部标记成黑色),堆空间启用写屏障,整个过程不要STW,效率高。
17.1 GO中的GC触发规则
-
系统触发:运行时自行根据内置的条件,检查、发现到,则进行 GC 处理,维护整个应用程序的可用性。
a. 使用系统监控,当超过两分钟没有产生任何GC时,强制触发 GC;
b.使用步调(Pacing)算法,其核心思想是控制内存增长的比例,当前内存分配达到一定比例则触发
-
手动触发:开发者在业务代码中自行调用
**runtime.GC()**
方法来触发 GC 行为。
17.2 标记清除算法
- 暂停程序业务逻辑(STW),分类出可达对象和不可达对象
- 找出所有可达对象,并作好标记
- 标记之后开始清除未标记的对象
- 停止暂停,让程序继续跑;然后重复以上步骤
17.3 三色标记法(插入屏障、删除屏障)
- 程序起初创建,全部标记为白色,放入白色集合中
- 遍历一次根结点,得到灰色节点,放入灰色标记表
- 遍历灰色标记表,将可达对象从白色标记为灰色,并将遍历后的灰色标记为黑色,放入黑色标记表
- 由于并发特性,此刻外界向在堆中或者栈中的对象发生添加对象操作,在堆中的对象会触发插入屏障机制,栈中的不触发
- 由于堆中对象插入屏障,会把堆中黑色对象添加的白色对象改成灰色,栈中的黑色对象添加的对象为白色
- 循环第5步,直到没有灰色节点;然后再准备回收白色节点前,加入STW暂停保护栈,防止外界干扰
- 将栈中的对象重新进行一次三色标记,直到没有灰色对象
- 停止STW,将白色对象清除
- 至于删除屏障,则是遍历灰色节点的时候出现可达节点被删除的时候,触发删除写屏障,将这个被删除的节点标记为灰色,等循环三色标记结束后,直到没有灰色节点,然后清理白色节点。
- 删除写屏障会造成一个对象即使被删除了最后一个指向它的指针也依旧可以活过这一轮,在下一轮GC中被清理。
17.4 混合写屏障规则
- GC开始将栈上的全部对象标记为黑色(之后不再进行第二次重复扫描,无需STW)
- GC期间,任何在栈上新创建的对象,均被标记为黑色
- 被删除的对象标记为灰色
- 被添加的对象标记为灰色
18.进程、线程、协程
进程: 进程是具有一定独立功能的程序,进程是系统资源分配和调度的最小单位。 每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。
线程: 线程是进程的一个实体,线程是内核态,而且是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。
协程: 协程是一种用户态的轻量级线程,协程的调度完全是由用户来控制的。协程拥有自己的寄存器上下文和栈。 协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。