golang卷卷面试题

1 Slice底层

1.1 数据结构

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

1.2 创建方式

  1. make创建

make创建,会从底层分配一个数组,数组长度就是指定的容量

golang卷卷面试题_第1张图片

  1. 使用数组创建

可以使用语句slice := array[5:7]来创建slice, cap就是剩下没有占用的数组长度

golang卷卷面试题_第2张图片

1.3 扩容机制

扩容的底层其实是重新分配空间, 再将原slice的数据拷贝到新的slice中, 然后返回新的slice

首先会进行预估扩容

从一个长度容量为2的int切片中, 使用append一次性插入4个int

那么这里预估扩容会先预估成容量为6, 进行判断, 如果大于两倍的旧容量, 则新切片的容量为6, 如果小于两倍的旧容量, 那么新容量就是两倍的旧容量, 也就是4

func main() {
	s := []int{1,2}
	s = append(s,3,4,5,6)
	fmt.Printf("len=%d, cap=%d",len(s),cap(s))
}

但是还有另一种情况

刚刚我们说的是int类型的切片, 它默认是我们的机器字长, 也就是8个字节, 但是切片类型是byte这样的, 小于八个字节, 会进行下一步

如果我从长度容量为200的byte切片中, 插入一个byte

按照预估扩容, 预估容量值为201, 小于200*2, 所以新的容量应该是400

但是并不是这样, 新的容量会是412

这里对于类型小于机器字长的类型, 会采用一种向上对齐的方式来分配容量

在go语言里, 内存都是按一些8的整数倍切成了不同大小的内存块

按照刚刚我们应该去申请一个400容量大小的byte切片, 数据部分要由一个400字节的连续内存, 但是因为没有400字节的内存块, 但是有412字节的内存块, go就会索性让你多扩充一点, 把整个412字节的内存都给你, 你的容量就是412了, 也就是一个向上取整的过程

2 Stirng类型

2.1 数据结构

type stringStruct struct {
    str unsafe.Pointer
    len int
}

2.2 编码方式

在有中文的情况下使用for i 和 for range的方式, 遍历效果不一样, 是因为string使用了utf-8的变长编码, 一个字符占多少字节是有该字符的头几位决定

golang卷卷面试题_第3张图片

for i的形式, 会按照字节流读取, for range 会按照字符流读取

2.3 string类型的修改

string在go语言里被分配到了只读内存, 不能进行修改

如需要修改的话,先转化为[]byte, 转化过程, 其实新开辟了一个等长的[]byte空间进行了拷贝, []byte可以修改, 改完之后再转回string即可

但是在[]byte转string的时候, 却不一定会发生拷贝, 在一些临时使用string类型的时候, 将[]byte转化为string, 会直接讲string的str指针指向该byte

  • map查找
func main() {
	m := map[string]int{"abc":1}
	b := []byte{'a','b','c'}
	value,has := m[string(b)]
}
  • 字符串拼接
func main() {
	b := []byte{'b'}
	abc := "a" + string(b) + "c"
}
  • 字符串比较
func main() {
	b := []byte{'acb'}
	a := string(b) == "abc"

}

3 Struct类型

3.1 内存对齐

因为64位的计算机, 在读取内存的时候, 是一次性读取八个字节出来, 所以传入的地址都是8的倍数, 如果想从地址1开始读8个字节, 就只能先从地址0开始, 读取八个字节, 再从地址8开始, 读取八个字节. 拼成从地址1读取八个字节

golang编译器为了减少这样的情况, 会把各种类型的数据安排到合适的地址, 占用合适的长度

同一种结构体, 内部成员一样, 但是顺序不同, 结构体占用的大小不一样, 正是因为内存对齐导致的

类型占用的初始地址, 必须是对齐边界的整数倍, 占用大小也需是对齐边界的整数倍

结构体内部成员的对齐边界取决于成员类型, 结构体的对齐边界取决于内部成员的最大对齐边界

3.2 空struct

因为空结构体不占据内存空间,因此被广泛作为各种场景下的占位符使用。

map 作为集合(Set)使用时,可以将值类型定义为空结构体,仅作为占位符使用即可。

不发送数据的信道(channel)
使用 channel 不需要发送任何的数据,只用来通知子协程(goroutine)执行任务,或只用来控制协程并发度。

4 Map类型

4.1 数据结构

4.1.1 hmap

包含了键值对的数目, 桶的数目

type hmap struct {
    count     int // count 字段表征了 map 目前的元素数目
    flags     uint8 //flag 字段标志 map 的状态, 如 map 当前正在被遍历或正在被写入
    B         uint8  // 哈希桶数目2^B
    noverflow uint16 // noverflow 是溢出桶的数目
    hash0     uint32 // hash0是随机哈希种子

    buckets    unsafe.Pointer // 是指向当前哈希桶的指针
    oldbuckets unsafe.Pointer //  是当桶扩容时指向旧桶的指针
    nevacuate  uintptr        // 是渐进式扩容时,桶进行调整时指示的搬迁进度

    extra *mapextra // optional fields
}

golang卷卷面试题_第4张图片
为什么是2^B次方的桶,难道6个桶、7个桶不行?

hash的方式目前主要就两种

  • 取模法 100%30=10这种
  • 与运算 hash & (2^B-1),假设B是2(3个桶0,1,2)那么 hash&0011,理论每个桶都有概率被选中。如果桶的数目是5(0101)那么低位的第二位始终是0,始终有桶是空桶(例如3号桶011)

4.1.2 bmap

buckets对应的数据结构

type bmap struct {
    topbits  [8]uint8
    keys     [8]keytype
    elems    [8]elemtype
    overflow uintptr
}

每个bucket可以存储8个键值对。

  • tophash是个长度为8的数组,哈希值相同的键存入当前bucket时会将哈希值的高8位存储在该数组中,以方便后续匹配,因为hash值是求余得出的,所以低位大概率相同,但是当B很小时,也会存在高八位相同但低位不同的情况,所以在key值匹配的之后,还是会看key值的具体值,这个tophash只起一轮筛选的作用。
  • data区存放的是key-value数据,存放顺序是key/key/key/…value/value/value,如此存放是为了节省字节对齐带来的空间浪费。
  • overflow 指针指向的是下一个bucket,据此将所有冲突的键连接起来。

golang卷卷面试题_第5张图片

4.1.3 mapextra

type mapextra struct {
    overflow    *[]*bmap // 目前桶中所有溢出桶的地址
    oldoverflow *[]*bmap // 旧桶中所有溢出桶的地址
    nextOverflow *bmap // 下一个空闲溢出桶地址
}

这里的下一个空闲溢出桶是这样的, 当B>4时, go会预先分配2^(B-4)个溢出桶作为备用
golang卷卷面试题_第6张图片

4.2 扩容机制

4.2.1 增量扩容

冲突太多,导致查找速率变慢,就需要对哈希表扩容,分配更多的桶,将旧桶里的数据分散到新桶中间,并且使用渐进式扩容,不是一次性将所有的旧桶数据转移到新桶,而是在每次操作map时,一次次搬运到新桶中。

负载因子=key的数目/桶的数量

当负载因子大于6.5时,再进行插入操作,会触发增量扩容

如图,此时已经有七个key,负载因子大于6.5,再次执行插入,触发扩容,新数据直接进新桶

golang卷卷面试题_第7张图片

hmap数据结构中oldbuckets成员指身原bucket,而buckets指向了新申请的bucket。新的键值对被插入新的bucket中。
后续对map的访问操作会触发迁移,将oldbuckets中的键值对逐步的搬迁过来。当oldbuckets中的键值对全部搬迁完毕后,删除oldbuckets。

4.2.2 等量扩容

溢出桶太多,也会触发扩容,将松散的key-value重新排列一次,减少溢出桶的数目,执行过很多del操作,会出现这样的情况

如果B<=15, 溢出桶数量 >= 2^B,就会触发等量扩容

如果B>15, 溢出桶只要 >=2^15,就会触发等量扩容

把旧桶的数据从搬运到新桶中,会重新排列,会排列的更加紧凑:

不增加buckets的数量

golang卷卷面试题_第8张图片

4.3 Map是线程安全的吗?

不是线程安全的, 但是go语言提供了一个sync.Map来用于多个goroutine来使用的Map

4.3.1 sync.Map

type Map struct {
	mu Mutex
	read atomic.Value // readOnly
	dirty map[interface{}]*entry //包含新写入的 key,并且包含 read 中所有未被删除的 key
	misses int //统计有多少次读取 read 没有被命中。每次 read 读取失败后,misses 的计数加 1
}

// Map.read 属性实际存储的是 readOnly。
type readOnly struct {
	m       map[interface{}]*entry
	amended bool //用于标记 read 和 dirty 的数据是否一致
}

type entry struct {
	p unsafe.Pointer // *interface{}
}

read 和 dirty 各自维护一套 key,key 指向的都是同一个 value。
也就是说,只要修改了这个 entry,对 read 和 dirty 都是可见的
golang卷卷面试题_第9张图片
现在向map操作Store(“hello”,“not world”),read中有对应的键,不会上锁访问dirty,直接替换
golang卷卷面试题_第10张图片
如果访问read中不存在的key,misses就加1,当misses大于某一个数时,就将dirty表升级为read表
dirty表置为nil

golang卷卷面试题_第11张图片

如果再次新增其他的键值对时,dirty表会进行重建,会复制read表中的数据
golang卷卷面试题_第12张图片
如果在dirty重建的时候,删除了某个键值对,会让entry指向nil,之后dirty重建,dirty中的key5也会复制,entry都指向nil, 但是其实这里的key5是不需要存在的
golang卷卷面试题_第13张图片
为了避免在重建dirty的时候复制了已经删除的key,会在dirty为nil的时候,删除某一个key时,进行entry指向expunged的操作
这样在重建dirty的时候回去判断该key下面是否真的有value
golang卷卷面试题_第14张图片

再次进行key5存储的时候,就需要看read表中有没有,如果有,是否标志为expunged,如果是,那么要去dirty表上锁,插入key5和与之对应的entry,同时更新read表中的key5的entry

golang卷卷面试题_第15张图片

5 函数

5.1 函数调用栈帧

首先函数被编译成一条条机器指令,放入虚拟内存地址中的代码段
golang卷卷面试题_第16张图片
函数调用另一个函数,会生成一条call指令
call指令会做两件事情, 一个是入栈返回地址, 一个是跳转到别的函数的代码段的入口
golang卷卷面试题_第17张图片
一个函数的开头会做三件事情, 挪动栈指针到预分配的栈帧顶部
将bp寄存器的调用者栈基存入栈中
更改栈基位置
golang卷卷面试题_第18张图片

函数结束的时候会执行这几步
恢复调用者栈基地址
释放自己的栈帧空间

golang卷卷面试题_第19张图片

然后就是执行ret指令
ret指令干两件事情
弹出返回地址
跳转到返回地址

golang卷卷面试题_第20张图片

5.2 函数的参数与返回值

5.2.1 匿名返回值

函数的会先将返回值空间入栈, 赋值0值
再将参数从右往左入栈
这里就一个参数,所以入栈a, a为0
注册完函数的这些参数和返回值后
就可以初始化incr函数的局部变量b
golang卷卷面试题_第21张图片
执行a++, a没有在函数incr的函数栈帧中
golang卷卷面试题_第22张图片
执行b=a
golang卷卷面试题_第23张图片
在return的时候, 会先对返回值空间赋值, 再执行defer函数, 所以先赋值
golang卷卷面试题_第24张图片
然后执行defer中的a++, b++
golang卷卷面试题_第25张图片
incr结束返回值就是1

5.2.2 命名返回值

值得注意的是, 命名返回值不是局部变量, 是函数的返回值空间有了名字, 可以被return之外的语句修改
main函数调用incr, 入栈返回值空间和参数
但是没有局部变量, 这里的b指向的是返回值空间
incr中返回的是a,但是也只是将a的值赋值给返回值空间
golang卷卷面试题_第26张图片
此时a++不影响返回值空间
但是b++影响, 因为b指向的就是返回值空间
golang卷卷面试题_第27张图片

5.3 函数的数据结构

函数对应的是一个Function Value的头等对象, 其本质是一个指针f, 指向funcval结构体
而funcval结构体中有个fn指针, 这个指针是指向了函数指令的入口地址
golang卷卷面试题_第28张图片
函数赋值给变量之后, 会让f1和f2指向同一个funcval结构体, 但是在闭包的情况, 不会指向一个funcval
golang卷卷面试题_第29张图片

5.3.1 闭包的原理

5.3.1.1 闭包捕获外部函数的局部变量

在第一次调用create时, 返回时生成一个funcval结构体,地址为addr2, 由于该匿名函数用了外层函数的参数, 所以生成捕获列表, 捕获了c变量
在第一次调用create时, 返回时也生成一个funcval结构体, 地址为addr2, 也生成捕获列表, 捕获了c变量
golang卷卷面试题_第30张图片
获取捕获列表的方式, 是通过寄存器保存funcval地址, 然后通过该寄存器的地址通过偏移找到捕获变量
这里的捕获变量没有遭到修改, 所以直接值拷贝到捕获列表中
但是要是捕获变量会修改, 就只会拷贝它的地址到捕获列表里
捕获变量真正的存储的地方逃逸到堆上

golang卷卷面试题_第31张图片

5.3.1.2 闭包捕获外部函数的参数

会先将参数拷贝到堆上, 然后捕获列表存堆上这个地址
golang卷卷面试题_第32张图片

5.3.1.3 闭包捕获外部函数的返回值

也会将返回值空间的数据拷贝到堆上来, 闭包使用该地址
但是在外部函数真正做返回的时候, 会把堆上的数据拷贝会栈上的返回值空间
golang卷卷面试题_第33张图片

6 Chan类型

6.1 数据结构

type hchan struct {
    qcount   uint           // 当前队列中剩余元素个数
    dataqsiz uint           // 环形队列长度,即可以存放的元素个数
    buf      unsafe.Pointer // 环形队列指针
    elemsize uint16         // 每个元素的大小
    closed   uint32            // 标识关闭状态
    elemtype *_type         // 元素类型
    sendx    uint           // 队列下标,指示元素写入时存放到队列中的位置
    recvx    uint           // 队列下标,指示元素从队列的该位置读出
    recvq    waitq          // 等待读消息的goroutine队列
    sendq    waitq          // 等待写消息的goroutine队列
    lock mutex              // 互斥锁,chan不允许并发读写
}

6.2 环形队列的实现

chan内部实现了一个环形队列作为其缓冲区,队列的长度是创建chan时指定的。

下图展示了一个可缓存6个元素的channel示意图:

golang卷卷面试题_第34张图片

  • dataqsiz指示了队列长度为6,即可缓存6个元素;
  • buf指向队列的内存,队列中还剩余两个元素;
  • qcount表示队列中还有两个元素;
  • sendx指示后续写入的数据存储的位置,取值[0, 6);
  • recvx指示从该位置读取数据, 取值[0, 6);

6.3 等待队列

从channel读数据,如果channel缓冲区为空或者没有缓冲区,当前goroutine会被阻塞。
向channel写数据,如果channel缓冲区已满或者没有缓冲区,当前goroutine会被阻塞。

被阻塞的goroutine将会挂在channel的等待队列中:

  • 因读阻塞的goroutine会被向channel写入数据的goroutine唤醒;
  • 因写阻塞的goroutine会被从channel读数据的goroutine唤醒;

6.4 写数据流程

golang卷卷面试题_第35张图片

6.5 读数据流程

golang卷卷面试题_第36张图片

6.6 关闭Chan

关闭channel时会把recvq中的G全部唤醒,本该写入G的数据位置为nil。把sendq中的G全部唤醒,但这些G会panic

6.7 range

通过range可以持续从channel中读出数据,好像在遍历一个数组一样,当channel中没有数据时会阻塞当前goroutine,与读channel时阻塞处理机制一样。

func chanRange(chanName chan int) {
    for e := range chanName {
        fmt.Printf("Get element from chan: %d\n", e)
    }
}

注意:如果向此channel写数据的goroutine退出时,系统检测到这种情况后会panic,否则range将会永久阻塞。

7 接口类型

7.1 类型系统

每种类型都对应着一些类型元数据, 内置类型的类型元数据里面有一些整体属性, 不同的类型有不同的其他描述信息, 而自定义类型还会有关于包路径, 方法数目, 方法等一些信息

7.2 别名与自定义类型的区别

别名生成的都是执行同一个类型元数据, 而自定义类型是指向不同的类型元数据

7.3 空接口的实现

对应eface

golang卷卷面试题_第37张图片

7.4 非空接口的实现

对应iface结构体, 包含itab和数据, itab包含了该接口的类型元数据, 和该接口的动态类型元数据

golang卷卷面试题_第38张图片

7.5 类型断言

分为四种情况,:

空接口断言成具体类型

非空接口断言成具体类型

空接口断言成非空接口

非空接口断言成非空接口

8 Mutex类型

8.1 数据结构

golang卷卷面试题_第39张图片

  • Locked: 表示该Mutex是否已被锁定,0:没有锁定 1:已被锁定。
  • Woken: 表示是否有协程已被唤醒,0:没有协程唤醒 1:已有协程唤醒,正在加锁过程中。
  • Starving:表示该Mutex是否处于饥饿状态,0:没有饥饿 1:饥饿状态,说明有协程阻塞了超过1ms。
  • Waiter: 表示阻塞等待锁的协程个数,协程解锁时根据此值来判断是否需要释放信号量。

8.2 自旋锁

加锁时,如果当前Locked位为1,说明该锁当前由其他协程持有,尝试加锁的协程并不是马上转入阻塞,而是会持续的探测Locked位是否变为0,这个过程即为自旋过程。

自旋时间很短,但如果在自旋过程中发现锁已被释放,那么协程可以立即获取锁。此时即便有协程被唤醒也无法获取锁,只能再次阻塞。

自旋的好处是,当加锁失败时不必立即转入阻塞,有一定机会获取到锁,这样可以避免协程的切换。

8.2.1 自旋条件

  • 自旋次数要足够小,通常为4,即自旋最多4次
  • CPU核数要大于1,否则自旋没有意义,因为此时不可能有其他协程释放锁
  • 协程调度机制中的Process数量要大于1,比如使用GOMAXPROCS()将处理器设置为1就不能启用自旋
  • 协程调度机制中的可运行队列必须为空,否则会延迟协程调度

8.3 Mutex模式

前面分析加锁和解锁过程中只关注了Waiter和Locked位的变化,现在我们看一下Starving位的作用。

每个Mutex都有两个模式,称为Normal和Starving。下面分别说明这两个模式。

8.3.1 normal模式

默认情况下,Mutex的模式为normal。

该模式下,协程如果加锁不成功不会立即转入阻塞排队,而是判断是否满足自旋的条件,如果满足则会启动自旋过程,尝试抢锁。

8.3.2 starvation模式

自旋过程中能抢到锁,一定意味着同一时刻有协程释放了锁,我们知道释放锁时如果发现有阻塞等待的协程,还会释放一个信号量来唤醒一个等待协程,被唤醒的协程得到CPU后开始运行,此时发现锁已被抢占了,自己只好再次阻塞,不过阻塞前会判断自上次阻塞到本次阻塞经过了多长时间,如果超过1ms的话,会将Mutex标记为”饥饿”模式,然后再阻塞。

处于饥饿模式下,不会启动自旋过程,也即一旦有协程释放了锁,那么一定会唤醒协程,被唤醒的协程将会成功获取锁,同时也会把等待计数减1。

8.3.3 Woken状态

Woken状态用于加锁和解锁过程的通信,举个例子,同一时刻,两个协程一个在加锁,一个在解锁,在加锁的协程可能在自旋过程中,此时把Woken标记为1,用于通知解锁协程不必释放信号量了,好比在说:你只管解锁好了,不必释放信号量,我马上就拿到锁了。

9 RWMutex

  1. 写锁需要阻塞写锁:一个协程拥有写锁时,其他协程写锁定需要阻塞
  2. 写锁需要阻塞读锁:一个协程拥有写锁时,其他协程读锁定需要阻塞
  3. 读锁需要阻塞写锁:一个协程拥有读锁时,其他协程写锁定需要阻塞
  4. 读锁不能阻塞读锁:一个协程拥有读锁时,其他协程也可以拥有读锁

9.1 数据结构

type RWMutex struct {
    w           Mutex  //用于控制多个写锁,获得写锁首先要获取该锁,如果有一个写锁在进行,那么再到来的写锁将会阻塞于此
    writerSem   uint32 //写阻塞等待的信号量,最后一个读者释放锁时会释放信号量
    readerSem   uint32 //读阻塞的协程等待的信号量,持有写锁的协程释放锁后会释放信号量
    readerCount int32  //记录读者个数
    readerWait  int32  //记录写阻塞时读者个数
}

9.2 方法逻辑

9.2.1 Lock()

写锁定

golang卷卷面试题_第40张图片

9.2.2 Unlock()

解除写锁

golang卷卷面试题_第41张图片

9.2.3 RLock()

读锁定

golang卷卷面试题_第42张图片

9.2.4 RUnlock()

解除读锁定

golang卷卷面试题_第43张图片

10 defer

把defer编译之后, 大体分两步, 先注册函数, 最后在return之前调用

在一个协程下注册的defer, 会被链到runtime.g 的 _defer链表里, 新注册的在头部插入, 执行的时候从头部执行

11 panic

panic触发时, 也会链在runtime.g下的panic链表, 只不不过输出异常信息的时候, 是从后往前输出

panic之后会触发defer函数, 之后return

12 recover

会将当前的panic的recovered字段置为true

13 Context

用来控制呈树结构的goroutine的, 让每个goroutine拥有相同的上下文

提供了一个Context接口

和四种实现

emptyCtx

timerCtx

cancelCtx

valueCtx

emptyCtx用Backround和TODO函数获得

timerCtx用withTimeOut和withDeadline获得

cancelCtx用withCancel获得

valueCtx用withValue获得

14 GMP模型

14.1 GMP分别是什么

  • G(Goroutine):即Go协程,每个go关键字都会创建一个协程。
  • M(Machine):工作线程,在Go中称为Machine,数量对应真实的CPU数(真正干活的对象)。
  • P(Processor):处理器(Go中定义的一个摡念,非CPU),包含运行Go代码的必要资源,用来调度 G 和 M 之间的关联关系,其数量可通过 GOMAXPROCS() 来设置,默认为核心数。

M必须拥有P才可以执行G中的代码,P含有一个包含多个G的队列,P可以调度G交由M执行。

14.2 调度策略

  • 队列轮转:P 会周期性的将G调度到M中执行,执行一段时间后,保存上下文,将G放到队列尾部,然后从队列中再取出一个G进行调度。除此之外,P还会周期性的查看全局队列是否有G等待调度到M中执行。

golang卷卷面试题_第44张图片

  • 系统调用:当G0即将进入系统调用时,M0将释放P,进而某个空闲的M1获取P,继续执行P队列中剩下的G。M1的来源有可能是M的缓存池,也可能是新建的。当G0系统调用结束后,如果有空闲的P,则获取一个P,继续执行G0。如果没有,则将G0放入全局队列,等待被其他的P调度。然后M0将进入缓存池睡眠

golang卷卷面试题_第45张图片

  • 抢占式调度

另一种情况是P所分配的任务G很快就执行完了(分配不均),这就导致了这个处理器P很忙,但是其他的P还有任务,此时如果global runqueue没有任务G了,那么P不得不从其他的P里拿一些G来执行。一般来说,如果P从其他的P那里要拿任务的话,一般就拿run queue的一半,这就确保了每个OS线程都能充分的使用,如下图:

golang卷卷面试题_第46张图片

15 内存管理

15.1 堆内存分配原理

golang卷卷面试题_第47张图片

16 垃圾回收原理

16.1 常见的垃圾回收算法:

  • 引用计数:每个对象维护一个引用计数,当被引用对象被创建或被赋值给其他对象时引用计数自动加 +1;如果这个对象被销毁,则计数 -1 ,当计数为 0 时,回收该对象。
    • 优点:对象可以很快被回收,不会出现内存耗尽或到达阀值才回收。
    • 缺点:不能很好的处理循环引用
  • 标记-清除:从根变量开始遍历所有引用的对象,引用的对象标记“被引用”,没有被标记的则进行回收。
    • 优点:解决了引用计数的缺点。
    • 缺点:需要 STW(stop the world),暂时停止程序运行。
  • 分代收集:按照对象生命周期长短划分不同的代空间,生命周期长的放入老年代,短的放入新生代,不同代有不同的回收算法和回收频率。
    • 优点:回收性能好
    • 缺点:算法复杂

16.2 三色标记法(标记清除法)

  • 初始状态下所有对象都是白色的。
  • 从根节点开始遍历所有对象,把遍历到的对象变成灰色对象
  • 遍历灰色对象,将灰色对象引用的对象也变成灰色对象,然后将遍历过的灰色对象变成黑色对象。
  • 循环步骤3,直到灰色对象全部变黑色。
  • 通过写屏障(write-barrier)检测对象有变化,重复以上操作
  • 收集所有白色对象(垃圾)。

16.3 STW(Stop The World)

  • 为了避免在 GC 的过程中,对象之间的引用关系发生新的变更,使得GC的结果发生错误(如GC过程中新增了一个引用,但是由于未扫描到该引用导致将被引用的对象清除了),停止所有正在运行的协程。
  • STW对性能有一些影响,Golang目前已经可以做到1ms以下的STW。

16.4 写屏障(Write Barrier)

  • 为了避免GC的过程中新修改的引用关系到GC的结果发生错误,我们需要进行STW。但是STW会影响程序的性能,所以我们要通过写屏障技术尽可能地缩短STW的时间。

造成引用对象丢失的条件:

一个黑色的节点A新增了指向白色节点C的引用,并且白色节点C没有除了A之外的其他灰色节点的引用,或者存在但是在GC过程中被删除了。以上两个条件需要同时满足:满足条件1时说明节点A已扫描完毕,A指向C的引用无法再被扫描到;满足条件2时说明白色节点C无其他灰色节点的引用了,即扫描结束后会被忽略 。

写屏障破坏两个条件其一即可

  • 破坏条件1:Dijistra写屏障(插入写屏障)

满足强三色不变性:黑色节点不允许引用白色节点 当黑色节点新增了白色节点的引用时,将对应的白色节点改为灰色

  • 破坏条件2:Yuasa写屏障(删除写屏障)

满足弱三色不变性:黑色节点允许引用白色节点,但是该白色节点有其他灰色节点间接的引用(确保不会被遗漏) 当白色节点被删除了一个引用时,悲观地认为它一定会被一个黑色节点新增引用,所以将它置为灰色

16.5 Golang GC过程

  • STW用于准备工作,如给每个P启动一个mark worker goroutine,启动写屏障,寻找root对象
  • 进行三色标记,同时用混合写屏障来辅助GC过程中的引用变化
  • STW 重新扫描部分根对象,禁止写屏障
  • 清扫阶段,将白色对象回收,归还给堆内存

16.6 回收时机

  1. 内存分配量达到阈值时触发: 每当内存扩大一倍时,会触发GC
  2. 定期触发: 默认是2分钟触发一次
  3. 手动触发: 使用runtime.GC()进行触发

逃逸分析

  • 指针逃逸
  • 栈空间不足逃逸
  • 动态类型逃逸
  • 闭包引用对象逃逸

你可能感兴趣的:(golang,数据结构,java,算法)