关于Go语言的底层,你想知道的都在这里!1

简介: 关于Go语言的底层,你想知道的都在这里!

1. GoLang语言

1.1 Slice

Slice底层实现原理

切片是基于数组实现的,它的底层是数组,它自己本身非常小,可以理解为对底层数组的抽象。因为基于数组实现,所以它的底层的内存是连续分配的,效率非常高,还可以通过索引获得数据,可以迭代以及垃圾回收优化。 切片本身并不是动态数组或者数组指针。它内部实现的数据结构通过指针引用底层数组,设定相关属性将数据读写操作限定在指定的区域内。切片本身是一 个只读对象,其工作机制类似数组指针的一种封装。


切片对象非常小,是因为它是只有 3 个字段的数据结构:


指向底层数组的指针


切片的长度


切片的容量


Slice扩容机制


在使用 append 向 slice 追加元素时,若 slice 空间不足则会发生扩容,扩容会重新分配一块更大的内存,将原 slice 拷贝到新 slice ,然后返回新 slice。扩容后再将数据追加进去。


扩容操作只对容量,扩容后的 slice 长度不变,容量变化规则如下:


若 slice 容量小于1024个元素,那么扩容的时候slice的cap就翻番,乘以2;一旦元素个数超过1024个元素,增长因子就变成1.25,即每次增加原来容量的四分之一。

若 slice 容量够用,则将新元素追加进去,slice.len++,返回原 slice

若 slice 容量不够用,将 slice 先扩容,扩容得到新 slice,将新元素追加进新 slice,slice.len++,返回新 slice。

Slice与数组区别


array是固定长度的数组,使用前必须确定数组长度,是值类型。


slice是一个引用类型,是一个动态的指向数组切片的指针。

slice是一个不定长的,总是指向底层的数组array的数据结构,可以动态扩容。


创建方式不一样,Slice使用make创建或者根据数组创建。


作为函数参数时,数组传递的是数组的副本,而slice传递的是指针。

1.2 Map

Map底层实现原理


Golang 中 map 的底层实现是一个散列表,因此实现 map 的过程实际上就是实现散表的过程。在这个散列表中,主要出现的结构体有两个,一个叫 hmap(a header for a go map),一个叫 bmap(a bucket for a Go map,通常叫其 bucket)。


hmap 哈希表

hmap是Go map的底层实现,每个hmap内都含有多个bmap(buckets桶、oldbuckets旧桶、overflow溢出桶),既每个哈希表都由多个桶组成。


buckets

buckets是一个指针,指向一个bmap数组,存储多个桶。


oldbuckets

oldbuckets是一个指针,指向一个bmap数组,存储多个旧桶,用于扩容。


overflow

overflow是一个指针,指向一个元素个数为2的数组,数组的类型是一个指针,指向一个slice,slice的元素是桶(bmap)的地址,这些桶都是溢出桶。为什么有两个?因为Go map在哈希冲突过多时,会发生扩容操作。[0]表示当前使用的溢出桶集合,[1]是在发生扩容时,保存了旧的溢出桶集合。overflow存在的意义在于防止溢出桶被gc。


bmap 哈希桶

bmap是一个隶属于hmap的结构体,一个桶(bmap)可以存储8个键值对。如果有第9个键值对被分配到该桶,那就需要再创建一个桶,通过overflow指针将两个桶连接起来。在hmap中,多个bmap桶通过overflow指针相连,组成一个链表。


Map进行有序的排序


map每次遍历,都会从一个随机值序号的桶,再从其中随机的cell开始遍历,并且扩容后,原来桶中的key会落到其他桶中,本身就会造成失序


如果想顺序遍历map,先把key放到切片排序,再按照key的顺序遍历map。


或者可以先把map中的key,通过sort包排序,再遍历map。


map 为什么是不安全的


Go map 默认是并发不安全的,同时对 map 进行并发读写的时,程序会 panic,原因如下:Go 官方经过长时间的讨论,认为 map 适配的场景应该是简单的(不需要从多个 gorountine 中进行安全访问的),而不是为了小部分情况(并发访问),导致大部分程序付出锁的代价,因此决定了不支持。


map 在扩缩容时,需要进行数据迁移,迁移的过程并没有采用锁机制防止并发操作,而是会对某个标识位标记为 1,表示此时正在迁移数据。如果有其他 goroutine 对 map 也进行写操作,当它检测到标识位为 1 时,将会直接 panic。


如果想实现map线程安全,有两种方式:


方式一:使用读写锁 map + sync.RWMutex


方式二:使用golang提供的 sync.Map


Map扩容策略


扩容时机:


向 map 插入新 key 的时候,会进行条件检测,符合下面这 2 个条件,就会触发扩容


扩容条件:


超过负载 map元素个数 > 6.5(负载因子) * 桶个数


溢出桶太多


当桶总数<2^15时,如果溢出桶总数>=桶总数,则认为溢出桶过多


当桶总数>215时,如果溢出桶总数>=215,则认为溢出桶过多


扩容机制:


双倍扩容:针对条件1,新建一个buckets数组,新的buckets大小是原来的2倍,然后旧buckets数据搬迁到新的buckets。


等量扩容:针对条件2,并不扩大容量,buckets数量维持不变,重新做一遍类似双倍扩容的搬迁动作,把松散的键值对重新排列一次,使得同一个 bucket 中的 key 排列地更紧密,节省空间,提高 bucket 利用率,进而保证更快的存取。


渐进式扩容:


插入修改删除key的时候,都会尝试进行搬迁桶的工作,每次都会检查oldbucket是否nil,如果不是nil则每次搬迁2个桶,蚂蚁搬家一样渐进式扩容


Map和Slice区别


数组:数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。声明方式:var a [3]int

slice(切片):Slice(切片)代表变长的序列,序列中每个元素都有相同的类型,slice的语法和数组很像,只是没有固定长度而已。

map:在Go语言中,一个map就是一个哈希表的引用,是一个无序的key/value对的集合

Map总结


map是引用类型

map遍历是无序的

map是非线程安全的

map的哈希冲突解决方式是链表法

map的扩容不是一定会新增空间,也有可能是只是做了内存整理

map的迁移是逐步进行的,在每次赋值时,会做至少一次迁移工作

map中删除key,有可能导致出现很多空的kv,这会导致迁移操作,如果可以避免,尽量避免


1.3 Channel


介绍一下Channel(有缓冲和无缓冲)


Go 语言中,不要通过共享内存来通信,而要通过通信来实现内存共享。Go 的CSP(Communicating Sequential Process)并发模型,中文可以叫做通信顺序进程,是通过 goroutine 和 channel 来实现的。


所以 channel 收发遵循先进先出 FIFO,分为有缓存和无缓存,channel 中大致有 buffer(当缓冲区大小部位 0 时,是个 ring buffer)、sendx 和 recvx 收发的位置(ring buffer 记录实现)、sendq、recvq 当前 channel 因为缓冲区不足 而阻塞的队列、使用双向链表存储、还有一个 mutex 锁控制并发、其他原属等。


// 无缓冲的channel由于没有缓冲发送和接收需要同步
ch := make(chan int)   
//有缓冲channel不要求发送和接收操作同步
ch := make(chan int, 2)  

channel 无缓冲时,发送阻塞直到数据被接收,接收阻塞直到读到数据;channel有缓冲时,当缓冲满时发送阻塞,当缓冲空时接收阻塞。


Channel实现原理


channel 内部维护了两个 goroutine 队列,一个是待发送数据的 goroutine 队列,另一个是待读取数据的 goroutine 队列。


每当对 channel 的读写操作超过了可缓冲的 goroutine 数量,那么当前的 goroutine 就会被挂到对应的队列上,直到有其他 goroutine 执行了与之相反的读写操作,将它重新唤起。


Channel读写流程


向 channel 写数据:


若等待接收队列 recvq 不为空,则缓冲区中无数据或无缓冲区,将直接从 recvq 取出 G ,并把数据写入,最后把该 G 唤醒,结束发送过程。


若缓冲区中有空余位置,则将数据写入缓冲区,结束发送过程。


若缓冲区中没有空余位置,则将发送数据写入 G,将当前 G 加入 sendq ,进入睡眠,等待被读 goroutine 唤醒。


从 channel 读数据


若等待发送队列 sendq 不为空,且没有缓冲区,直接从 sendq 中取出 G ,把 G 中数据读出,最后把 G 唤醒,结束读取过程。


如果等待发送队列 sendq 不为空,说明缓冲区已满,从缓冲区中首部读出数据,把 G 中数据写入缓冲区尾部,把 G 唤醒,结束读取过程。


如果缓冲区中有数据,则从缓冲区取出数据,结束读取过程。


将当前 goroutine 加入 recvq ,进入睡眠,等待被写 goroutine 唤醒。


关闭 channel


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


panic 出现的场景还有:


关闭值为 nil 的 channel

关闭已经关闭的 channel

向已经关闭的 channel 中写数据

Channel为什么能做到线程安全


Channel 可以理解是一个先进先出的队列,通过管道进行通信,发送一个数据到Channel和从Channel接收一个数据都是原子性的。不要通过共享内存来通信,而是通过通信来共享内存,前者就是传统的加锁,后者就是Channel。设计Channel的主要目的就是在多任务间传递数据的,本身就是安全的。


Channel是同步进行还是异步的(Channel的三种状态)


Channel是异步进行的, channel存在3种状态:


nil,未初始化的状态,只进行了声明,或者手动赋值为nil

active,正常的channel,可读或者可写

closed,已关闭,千万不要误认为关闭channel后,channel的值是nil

操作 一个零值nil通道 一个非零值但已关闭的通道 一个非零值且尚未关闭的通道
关闭 产生恐慌 产生恐慌 成功关闭
发送数据 永久阻塞 产生恐慌 阻塞或者成功发送
接收数据 永久阻塞 永不阻塞 阻塞或者成功接收

给一个 nil channel 发送数据,造成永远阻塞

从一个 nil channel 接收数据,造成永远阻塞

给一个已经关闭的 channel 发送数据,引起 panic

从一个已经关闭的 channel 接收数据,如果缓冲区中为空,则返回一个零值

无缓冲的 channel 是同步的,而有缓冲的 channel 是非同步的

关闭一个 nil channel 将会发生 panic


20.png


1.4 Goroutine

进程、线程和协程的区别


进程: 进程是具有一定独立功能的程序,进程是系统资源分配和调度的最小单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。


线程: 线程是进程的一个实体,线程是内核态,而且是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。


协程: 协程是一种用户态的轻量级线程,协程的调度完全是由用户来控制的。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。


线程和协程的区别


线程切换需要陷入内核,然后进行上下文切换,而协程在用户态由协程调度器完成,不需要陷入内核,这样代价就小了。

协程的切换时间点是由调度器决定,而不是由系统内核决定的,尽管它们的切换点都是时间片超过一定阈值,或者是进入I/O或睡眠等状态时。

基于垃圾回收的考虑,Go实现了垃圾回收,但垃圾回收的必要条件是内存位于一致状态,因此就需要暂停所有的线程。如果交给系统去做,那么会暂停所有的线程使其一致。对于Go语言来说,调度器知道什么时候内存位于一致状态,所以也就没有必要暂停所有运行的线程。

介绍一下Goroutine


Goroutine 是一个与其他 goroutines 并行运行在同一地址空间的 Go 函数或方法。


goroutine的概念类似于线程,但 goroutine是由Go的运行时(runtime)调度和管理的。Go程序会智能地将 goroutine 中的任务合理地分配给每个CPU。它在语言层面已经内置了调度和上下文切换的机制。


goroutine是Go并发设计的核心,也叫协程,它比线程更加轻量,因此可以同时运行成千上万个并发任务。在Go语言中,每一个并发的执行单元叫作一个goroutine。我们只需要在调用的函数前面添加go关键字,就能使这个函数以协程的方式运行。


context包结构原理和用途


Context(上下文)是Golang应用开发常用的并发控制技术 ,它可以控制一组呈树状结构的goroutine,每个goroutine拥有相同的上下文。Context 是并发安全的,主要是用于控制多个协程之间的协作、取消操作。


Context 只定义了接口,凡是实现该接口的类都可称为是一种 context。


「Deadline」 方法:可以获取设置的截止时间,返回值 deadline 是截止时间,到了这个时间,Context 会自动发起取消请求,返回值 ok 表示是否设置了截止时间。

「Done」 方法:返回一个只读的 channel ,类型为 struct{}。如果这个 chan 可以读取,说明已经发出了取消信号,可以做清理操作,然后退出协程,释放资源。

「Err」 方法:返回Context 被取消的原因。

「Value」 方法:获取 Context 上绑定的值,是一个键值对,通过 key 来获取对应的值。

goroutine调度


GPM是Go语言运行时(runtime)层面的实现,是go语言自己实现的一套调度系统。区别于操作系统调度OS线程。


G很好理解,就是个goroutine的,里面除了存放本goroutine信息外 还有与所在P的绑定等信息。

P管理着一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务。

M(machine)是Go运行时(runtime)对操作系统内核线程的虚拟, M与内核线程一般是一一映射的关系, 一个groutine最终是要放到M上执行的;

P与M一般也是一一对应的。他们关系是: P管理着一组G挂载在M上运行。当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G 挂载在新建的M上。当旧的G阻塞完成或者认为其已经死掉时 回收旧的M。


P的个数是通过runtime.GOMAXPROCS设定(最大256),Go1.5版本之后默认为物理线程数。 在并发量大的时候会增加一些P和M,但不会太多,切换太频繁的话得不偿失。


单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的,goroutine则是由Go运行时(runtime)自己的调度器调度的,这个调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。 其一大特点是goroutine的调度是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身goroutine的超轻量,以上种种保证了go调度方面的性能。


如何避免Goroutine泄露和泄露场景


gorouinte 里有关于 channel 的操作,如果没有正确处理 channel 的读取,会导致 channel 一直阻塞住, goroutine 不能正常结束


waitgroup 用法和原理


waitgroup 内部维护了一个计数器,当调用 wg.Add(1) 方法时,就会增加对应的数量;当调用 wg.Done() 时,计数器就会减一。直到计数器的数量减到 0 时,就会调用

runtime_Semrelease 唤起之前因为 wg.Wait() 而阻塞住的 goroutine。


使用方法:


main 协程通过调用 wg.Add(delta int) 设置 worker 协程的个数,然后创建 worker 协程;

worker 协程执行结束以后,都要调用 wg.Done();

main 协程调用 wg.Wait() 且被 block,直到所有 worker 协程全部执行结束后返回。

实现原理:


WaitGroup 主要维护了 2 个计数器,一个是请求计数器 v,一个是等待计数器 w,二者组成一个 64bit 的值,请求计数器占高 32bit,等待计数器占低 32bit。


每次 Add 执行,请求计数器 v 加 1,Done 方法执行,请求计数器减 1,v为0 时通过信号量唤醒 Wait()。


1.5 GMP调度

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执行。


优先从 P 的本地队列获取 goroutine 来执行;如果本地队列没有,从全局队列获取,如果全局队列也没有,会从其他的 P 上偷取 goroutine。


GMP goroutine调度策略


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

系统调用:当G0即将进入系统调用时,M0将释放P,进而某个空闲的M1获取P,继续执行P队列中剩下的G。M1的来源有可能是M的缓存池,也可能是新建的。

当G0系统调用结束后,如果有空闲的P,则获取一个P,继续执行G0。如果没有,则将G0放入全局队列,等待被其他的P调度。然后M0将进入缓存池睡眠。

调度器的设计策略


复用线程:避免频繁的创建、销毁线程,而是对线程的复用。


work stealing 机制

当本线程无可运行的 G 时,尝试从其他线程绑定的 P 偷取 G,而不是销毁线程。

hand off 机制

当本线程因为 G 进行系统调用阻塞时,线程释放绑定的 P,把 P 转移给其他空闲的线程执行。

利用并行:GOMAXPROCS 设置 P 的数量,最多有 GOMAXPROCS 个线程分布在多个 CPU 上同时运行。GOMAXPROCS 也限制了并发的程度,比如 GOMAXPROCS = 核数/2,则最多利用了一半的 CPU 核进行并行。


抢占:在 coroutine 中要等待一个协程主动让出 CPU 才执行下一个协程,在 Go 中,一个 goroutine 最多占用 CPU 10ms,防止其他 goroutine 被饿死,这就是 goroutine 不同于 coroutine 的一个地方。


全局 G 队列:,当 M 执行 work stealing 从其他 P 偷不到 G 时,它可以从全局 G 队列获取 G。


CSP 模型是“以通信的方式来共享内存”,不同于传统的多线程通过共享内存来通信。用于描述两个独立的并发实体通过共享的通讯 channel (管道)进行通信的并发模型。


两种抢占式调度


协作式的抢占式调度


在 1.14 版本之前,程序只能依靠 Goroutine 主动让出 CPU 资源才能触发调度,存在问题


某些 Goroutine 可以长时间占用线程,造成其它 Goroutine 的饥饿


垃圾回收需要暂停整个程序(Stop-the-world,STW),最长可能需要几分钟的时间,导致整个程序无法工作。


基于信号的抢占式调度


在任何情况下,Go 运行时并行执行(注意,不是并发)的 goroutines 数量是小于等于 P 的数量的。为了提高系统的性能,P 的数量肯定不是越小越好,所以官方默认值就是 CPU 的核心数,设置的过小的话,如果一个持有 P 的 M, 由于 P 当前执行的 G 调用了 syscall 而导致 M 被阻塞,那么此时关键点:GO 的调度器是迟钝的,它很可能什么都没做,直到 M 阻塞了相当长时间以后,才会发现有一个 P/M 被 syscall 阻塞了。然后,才会用空闲的 M 来强这个 P。通过 sysmon 监控实现的抢占式调度,最快在 20us,最慢在 10-20ms 才 会发现有一个 M 持有 P 并阻塞了。操作系统在 1ms 内可以完成很多次线程调度(一般情况 1ms 可以完成几十次线程调度),Go 发起 IO/syscall 的时候执行该 G 的 M 会阻塞然后被 OS 调度走,P 什么也不干,sysmon 最慢要 10-20ms才能发现这个阻塞,说不定那时候阻塞已经结束了,宝贵的 P 资源就这么被阻塞的 M 浪费。


GMP 调度过程中存在哪些阻塞


I/O,select


block on syscall


channel


等待锁


runtime.Gosched()


GMP 调度流程


每个 P 有个局部队列,局部队列保存待执行的 goroutine(流程 2),当 M 绑 定的 P 的的局部队列已经满了之后就会把 goroutine 放到全局队列(流程 2- 1)


每个 P 和一个 M 绑定,M 是真正的执行 P 中 goroutine 的实体(流程 3),M 从绑定的 P 中的局部队列获取 G 来执行


当 M 绑定的 P 的局部队列为空时,M 会从全局队列获取到本地队列来执行G(流程 3.1),当从全局队列中没有获取到可执行的 G 时候,M 会从其他 P 的局部队列中偷取 G 来执行(流程 3.2),这种从其他 P 偷的方式称为 work stealing


当 G 因系统调用(syscall)阻塞时会阻塞 M,此时 P 会和 M 解绑即 hand off,并寻找新的 idle 的 M,若没有 idle 的 M 就会新建一个 M(流程 5.1)。


当 G 因 channel 或者 network I/O 阻塞时,不会阻塞 M,M 会寻找其他 runnable 的 G;当阻塞的 G 恢复后会重新进入 runnable 进入 P 队列等待执 行(流程 5.3)


1.6 垃圾回收机制

GC 原理


垃圾回收就是对程序中不再使用的内存资源进行自动回收的操作。


三色标记法


初始状态下所有对象都是白色的。

从根节点开始遍历所有对象,把遍历到的对象变成灰色对象

遍历灰色对象,将灰色对象引用的对象也变成灰色对象,然后将遍历过的灰色对象变成黑色对象。

循环步骤3,直到灰色对象全部变黑色。

通过写屏障(write-barrier)检测对象有变化,重复以上操作

收集所有白色对象(垃圾)。

STW(Stop The World)


为了避免在 GC 的过程中,对象之间的引用关系发生新的变更,使得GC的结果发生错误(如GC过程中新增了一个引用,但是由于未扫描到该引用导致将被引用的对象清除了),停止所有正在运行的协程。

STW对性能有一些影响,Golang目前已经可以做到1ms以下的STW。

GC 的触发条件


主动触发(手动触发),通过调用 runtime.GC 来触发GC,此调用阻塞式地等待当前GC运行完毕。

被动触发,分为两种方式:


使用步调(Pacing)算法,其核心思想是控制内存增长的比例,每次内存分配时检查当前内存分配量是否已达到阈值(环境变量GOGC):默认100%,即当内存扩大一倍时启用GC。

使用系统监控,当超过两分钟没有产生任何GC时,强制触发 GC。

Golang为什么小对象多了会造成gc压力


通常小对象过多会导致GC三色法消耗过多的GPU。优化思路是,减少对象分配。


GC的屏障介绍


写屏障(Write Barrier)


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

写屏障:并发gc会产生黑色节点引用白色节点情况,导致正常的指针变量错误的被清除;解决方法为写屏障;


主要包括强三色不变式和弱三色不变式;


强三色不变:黑色节点不能引用白色节点,如果引用白色节点需要将白色节点置灰(插入写屏障);


弱三色不变:黑节点可以引用白节点,但白节点有其他灰色节点或递归指向存在灰色节点,删除白色节点引用时,需要把白色节点置灰(删除写屏障);


栈上变量较小,且频繁开辟或删除,不开启写屏障;需要之后一次rescan;


stw时机:


   插入写屏障:结束时需要STW来重新扫描栈,标记栈上引用的白色对象的存活;(1.5版本采用)

   删除写屏障:回收精度低,GC开始时STW扫描堆栈来记录初始快照,这个过程会保护开始时刻的所有存活对象;

1

2

混合写屏障:1.8版本加入


原因是stw需要耗时;加入混合写屏障,解决这个问题;


流程:


1.开始标记时候,栈上可达节点均置黑,之后不进行rescan,不用stw;


2.gc时产生的在栈上创建的对象,均置黑;


3.堆空间删除的对象置灰;


4.堆空间插入的对象置灰;


特点


混合写屏障继承了插入写屏障的优点,起始无需 STW 打快照,直接并发扫描垃圾即可;


混合写屏障继承了删除写屏障的优点,赋值器是黑色赋值器,GC 期间,任何在栈上创建的新对象,均为黑色。扫描过一次就不需要扫描了,这样就消除了插入写屏障时期最后 STW 的重新扫描栈;


混合写屏障扫描精度继承了删除写屏障,比插入写屏障更低,随着带来的是 GC 过程全程无 STW;


混合写屏障扫描栈虽然没有 STW,但是扫描某一个具体的栈的时候,还是要停止这个 goroutine 赋值器的工作的哈(针对一个 goroutine 栈来说,是暂停扫的,要么全灰,要么全黑哈,原子状态切换)。


GC 的流程是什么


当前版本的 Go 以 STW 为界限,可以将 GC 划分为五个阶段:


阶段说明赋值器状态 GCMark 标记准备阶段,为并发标记做准备工作,启动写屏障 STWGCMark 扫描标记阶段,与赋值器并发执行,写屏障开启并发


GCMarkTermination 标记终止阶段,保证一个周期内标记任务完成,停止写屏障 STWGCoff 内存清扫阶段,将需要回收的内存归还到堆中,写屏障关闭并发


GCoff 内存归还阶段,将过多的内存归还给操作系统,写屏障关闭并发


GC 如何调优


优化内存的申请速度,尽可能少申请内存,复用已申请的内存。三个关键字:控制、减少、复用。


通过 go tool pprof 和 go tool trace 等工具


控制内存分配的速度,限制 goroutine 的数量,从而提高赋值器对 CPU 的利用率。

减少并复用内存,例如使用 sync.Pool 来复用需要频繁创建临时对象,例如提前分配足够的内存来降低多余的拷贝。

需要时,增大 GOGC 的值,降低 GC 的运行频率。


相关文章
|
6天前
|
安全 测试技术 Go
Go语言在高并发场景下的应用
在当今互联网高速发展的时代,高并发已成为众多应用系统面临的核心问题。本文探讨了Go语言在高并发场景下的优势,并通过具体实例展示了其在实际应用中的效果和性能表现。
|
3天前
|
Go
go语言map、实现set
go语言map、实现set
9 0
|
3天前
|
Go
go语言数组与切片
go语言数组与切片
11 0
|
5天前
|
存储 中间件 Go
在go语言服务中封装路由和示例
【6月更文挑战第23天】本文介绍golang后端按协议处理、中间件(一次性与每次请求执行)划分、以及服务架构Controller、Logic/Service、DAO/Repository和Routers划分。代码仓库在GitHub上提供。使用框架简化了交互和处理。后续章节深入探讨服务构建。
104 5
在go语言服务中封装路由和示例
|
1天前
|
Devops Go 云计算
Go语言发展现状:历史、应用、优势与挑战
Go语言发展现状:历史、应用、优势与挑战
10 2
|
3天前
|
Go
go语言的hello,world
go语言的hello,world
8 1
|
6天前
|
Unix Go 开发者
探索Go语言并发模型:原理与实践
本文深入探讨了Go语言的并发模型,包括其设计原理、Goroutine和Channel的基本使用,以及如何在实际项目中高效地应用这些概念来提高程序的性能和可维护性。
|
7天前
|
Go
Go 语言是如何实现切片扩容
Go 语言是如何实现切片扩容
|
8天前
|
存储 Go
Go 语言当中 CHANNEL 缓冲
Go 语言当中 CHANNEL 缓冲
|
8天前
|
中间件 Go
go语言后端开发学习(三)——基于validator包实现接口校验
go语言后端开发学习(三)——基于validator包实现接口校验