关于后端:Go相关高频面试题

9次阅读

共计 11768 个字符,预计需要花费 30 分钟才能阅读完成。

注: 此系列内容来自网络,未能查到原作者。感觉不错,在此分享。

pprof 应用流程:https://time.geekbang.org/column/article/410205 https://time.geekbang.org/column/article/408529

pprof(堆、哪个办法占用 cpu 工夫多,耗时、协程数、零碎线程数、阻塞耗时 block、互斥锁耗时 mutex)。go tool pprof 连贯进行 pprof 采集,默认采集 30 秒。pprof 可剖析 go 程序 cpu 占用过高问题或零碎线程数过多 cpu 负载高。采样图剖析:采样图中,矩形框面积越大,阐明这个函数的累积采样工夫越大。如果一个函数剖析采样图中的矩形框面积很大,这时候咱们就要认真剖析了,因为很可能这个函数就有须要优化性能的中央。火焰图剖析:每一列代表一个调用栈,每一个格子代表一个函数。纵轴展现了栈的深度,依照调用关系从上到下排列。最上面的格子代表采样时,正在占用 CPU 的函数。调用栈在横向会依照字母排序,并且同样的调用栈会做合并,所以一个格子的宽度越大,阐明这个函数越可能是瓶颈。耗时剖析(终端交互):top 命令默认是按 flat% 排序的。做性能剖析时,先依照 cum 来排序,top -cum,能够直观地看到哪个函数总耗时最多,而后参考该函数的本地采样工夫和调用关系。判断是该函数性能耗时多,还是它调用的函数耗时多。本地采样工夫占比很小,然而累积采样工夫占比很高,这个函数耗时多是因为调用了其余函数,它们本身简直没有耗时。而后用 `list 办法名 ` 命令查看具体的外部耗时状况。内存剖析:内存剖析同上,本地采样内存占比很小,然而累积采样内存占比很高,这个函数多是因为调用的其余函数费内存,它们本身简直没有内存耗费。go trace:能够剖析 GC 问题,依据 GC 反查到哪个办法在大量生成内存触发 GC。但如果您想跟踪运行迟缓的函数,或找到 CPU 工夫破费在哪,这个工具就不适合了。您应该应用 go tool pprof,它能显示在每个函数中破费的 CPU 工夫的百分比。go tool trace 更适宜找出程序在一段时间内正在做什么,而不是总体上的开销。

go 程序启动流程:

初始化并绑定 m0 与 g0,m0 是主线程,程序启动必然会有个主线程 m0,g0 负责调度:调用 schedule()函数;创立 P,绑定 m0 和 p0,先创立 GOMAXPROCS 个 P,存在 sched 闲暇链表 (Pidle);m0 的 g0 会创立一个指向 runtime.main() 的 g,并放到 p0 的本地队列;runtime.main():启动 sysmon 线程、启动 GC 协程、执行 init(各种 init 函数)、执行 main.main 函数;新 G 将唤醒 Pidle 里的 P,以更好地散发工作。P 会找一个自旋状态的 M / 唤醒睡眠的 M / 创立 M 与之关联,而后切换 g0 进行 schedule 调度。

golang 调度:runnext > 本地队列 > 全局队列 > 网络 > (从其余 P 的本地队列中)偷取

1、从本地队列拿,2、没有从全局队列里拿一半回本地队列,3、从网络轮询器里拿并 Inject 到全局队列,4、从别的 P 偷一半放到本地,注:每个 P 每 61 次都会从全局队列里拿一个,防全局队列里的 G 饿死。newm:创立一个工作线程,调用 mstart,再调用 minit 和 mstartm0 做信号初始化工作 (注册信号抢占函数),最初调用 schedule()。每个 M 都有一个 schedule() 做死循环的用 g0 进行 G 的调度。当 G 运行完结时,会先放到 p.gFree,最多 64 个,满了放一半到 sche.gFree 闲暇链表里,当再 go fun 时会先从 gFree 里拿旧的 G,没有再创立新的 G 对象。schedule()函数:一开始获取的_g_ 是 g0 来的,从 g0 上获取对应的 m 到 p 拿到 G 后,调用 gogo 从 g0 栈切换到 G 栈后执行工作,执行完后调用 goexit 到 mcall 把 G 栈切换回 g0 栈,最初调用 goexit0 把 G 放回 gFree。而后从头再来,获取_g_拿到 g0 循环这样解决。g0 的作用是帮助调度的协程。g->g0 10us、findG 20-100us、g0->next G 10us

gopark 的作用:https://blog.csdn.net/u010853261/article/details/85887948

解除以后 G 和 M 的绑定关系,将以后 G 状态机切换为_Gwaiting;调用一次 schedule(),调度器 P 发动一轮新的调度;

mcall()的作用:保留以后协程信息 (PC/SP 存到 g ->sched),调用 goready 时能复原现场;以后线程从 g 堆栈切换到 g0 堆栈;在 g0 的堆栈上执行函数 fn(g);gopark 里的 park_m 是在切换到 g0 堆栈上后调度 schedule(),schdeule() 能够从 g0 开始进行 G 的调度;

goready 的作用:将 G 的状态机切换到_Grunnable,而后退出到 P 的调度器的本地队列,期待 P 进行调度。

对于 mainG,执行完用户定义的 main 函数的所有代码后,间接调用 exit(0)退出整个过程,很王道,不论其余 G 是否解决实现。对于一般 G 在执行完代码,先是跳转到提前设置好的 goexit 函数的第二条指令,而后调用 runtime.goexit1,接着调用 mcall(goexit0),会切换到 g0 栈,运行 goexit0 函数,清理 G 的一些字段,并将其增加到 G 缓存池 (gFree) 里,而后进入 schedule 调度循环。到这里,一般 goroutine 才算完成使命。

Sysmon 监控线程:

处于 _Prunning 或者 _Psyscall 状态的 P,如果上一次触发调度的工夫曾经过来了 10ms,咱们会通过 runtime.preemptone 抢占以后处理器。处于_Psyscall 状态的 P,当零碎调用超过 10ms,且队列有其余 G 要解决或不存在闲暇处理器,Sysmon 会调用 runtime.handoffp 让出 P 的使用权,把 M 和 G 独立进来运行。留神:抢占 P 的使用权和让出 P 的使用权是不一样的。留神零碎调用时线程池耗尽问题,Go 里 M 最多 1w 个。1.13 依赖栈增长检测代码形式:编译器在有显著栈耗费的函数头部插入一些检测代码,用 g.stackguard0 判断是否须要进行栈增长,如果 g.stackguard0 被设置为非凡标识 runtime.stackPreempt 便不会执行栈增长而去执行一次 schedule()。空的 for 循环并没有调用函数,就没机会执行栈增长检测代码,他并不知道 GC 在期待他让出。1.14 基于信号的抢占调度:在创立一个工作线程时,会调用 mstart,调用 minit 做信号初始化(注册信号抢占函数),产生抢占时会调用 preemptone 做信号异步抢占,preemptone->preemptM->signalM(向指定 M 发送 sigPreemt 信号)->SYS_tgkill。收到信号后会调用 asyncPreempt2 调 mcall 和 gopreempt_m


2 分钟触发次 GC。若刚开始生成大量堆内存,GC 后的残余内存变多 GOGC 为 100,则 1G 变 2G 才触发,2G 变 4G 才触发。所以要定期 2 分钟清理。周期性扫描网络轮询器,把监听到数据的 goroutine 注入到全局队列

golang Mutex 同步原语: https://my.oschina.net/u/142293/blog/5012739

互斥锁在失常状况下是把 G 切换至_Gwaiting,期待锁的持有者唤醒协程,把 G gopark 放到 semaroot 的 sudog 链表里挂着,解锁时会调用 goready 期待 P 调度,协程锁是基于线程锁的,可能会陷入内核挂起。读写锁是写优先于读的,防读锁过多导致写始终阻塞,读多写少 RWMutex。写多读少 mutex 就行。semaRoot(semtable 会有 251 个 semaRoot 均衡树,每一个 sync.Mutex 的 sema 地址对应一个 semaRoot, 雷同的锁的 sudog 由链表串起来) semaRoot 则是队列构造体,外部是堆树,把和以后 G 关联的 sudog 放到 semaRoot 里,而后把 G 的状态改为_Gwaiting,调用调度器执行别的 G,此时以后 G 就进行执行了。始终到被调度器从新调度执行,会首先开释 sudog 而后再去执行别的代码逻辑,semaRoot 里有 runtime.mutex 线程锁的,要保障在面临多线程并发时,数据同步的问题。sync.Mutex 与 runtime.mutex 区别,Mutex 挂起协程,不肯定阻塞线程。mutex 是挂起线程,调度下一个线程执行。sync.Mutex=>gopark(),runtime.mutex=>futex()。sync.Mutex 是设计给 goroutine 的,但 goroutine 是在用户空间实现的,但内核只能看见一组线程。协程同步的根底是线程同步,只是可能自旋不必挂起线程了,临界区内实现期待队列这类敏感数据的操作就行了。就是说 runtime.mutex 是线程锁,其底层是 futex 零碎调用,但加锁工夫往往很短。留神 goparkUnlock 是在这里解锁线程锁的。

golang defer 原理:https://zhuanlan.zhihu.com/p/261840318

defer 语句在以后 G 构造体的 g._defer 链表把 defer 构造体串起来,新 defer 是推入链表头部的。在有 defer 的函数开端会插入 runtime.deferreturn 函数来调用 defer 链;函数 return 执行流程:1. 先给返回值赋值(匿名和有名返回值),2. 以后 G 的 g._defer 链表的头开始执行 defer 构造里指向的函数,3. 包裹函数 return 返回。defer 在 1.14 版本的优化:编译阶段把 defer 代码块插入尾部(openCodedDefer 相似内联),但不适宜循环中 defer,循环中 defer 还是会插入 g._defer 链表

golang panic/recover 原理:g._defer 链表、g._panic 链表(https://draveness.me/golang/docs/part2-foundation/ch05-keywor…)

编译器会做转换关键字的工作,panic 和 recover 转换成 runtime.gopanic 和 runtime.gorecover;defer 转换成 runtime.deferproc 函数;panic 时,执行 gopanic 函数,创立新 runtime._panic 增加到所在 G 的 g._panic 链表头部,而后遍历 g._defer 链调用提早函数,最初调用 fatalpanic 执行 g._panic 停止整个程序;defer 与 panic:先解决完 g._defer 链上的工作,结束后再执行 g._panic 再退出整个程序,停掉执行中的程序。panic 只会触发以后 G 的 defer,recover 只有 g._panic 链有值时执行才会失效,即 recover 写在最开始的 defer 语句中;panic 容许在 defer 中嵌套屡次调用;panic 与 recover:panic 时调用 g._defer 链,若遇到了 gorecover 会将_panic.recovered 标记成 true 并返回 panic 的参数,会跳到 deferreturn 函数持续遍历 g._defer 链,不再执行 fatalpanic 停止程序,就从 panic 中复原并执行失常的逻辑;若没遇到 gorecover 会遍历 g._defer 链,最初调 fatalpanic 停止程序。

golang Channel 构造:

lock(runtime.mutex 线程锁)、buff、qcount(缓冲区现有多少元素)、buffSize(缓冲区总大小)、closed(敞开状态)、sendx/recvx(队列下标用于数组循环队列)、sendq(期待写音讯队列)、recvq(期待读音讯队列)、elemtype(元素的类型)

写:如果缓冲区中没有空余地位,将待发送数据写入 G,将以后 G 退出 sendq,进入睡眠,期待被读 goroutine 唤醒;

读:如果缓冲区中没有数据,将以后 goroutine 退出 recvq,进入睡眠,期待被写 goroutine 唤醒;

Channel 里是有 runtime.mutex 线程锁的,读写时会 lock 一下,因为读写存在缓冲区的设计,并没有间接应用 sync.Mutex,而是在 mutex 的根底上,减少 waitq、writeq 本人实现了一套排队逻辑,保障多线程并发。

golang Map 构造:hashmap,是应用数组 +bmap 链表实现的,用拉链法打消 hash 抵触

key 通过 hash 后失去一个数,其低 8 位用于抉择 bucket 桶,高 8 位用于放在 bmap 的[8]uint8 数组中,用于疾速碰撞试错。而后一个 bmap 指向下一个 bmap 即溢出桶(拉链)。

map 并发问题:入源代码写抵触 h.flags ^= hashWriting。hash 抵触后元素是优先放头部,新写数据被马上拜访的概率越大

扩容:扩容桶会翻倍(B+1),渐进式扩容,当拜访到具体的某个 bucket 的时候,会把旧 bucket 中的数据转移到新的 bucket 中。一个 bucket 一个 bucket 的进行迁徙,能够避免被锁住长时间不可拜访(性能抖动)。扩容因子: hmap.count> 桶数量 (2^B)*6.5(不包含溢出桶),无奈缩容。等量扩容(伪缩容):为了缩小溢出桶应用(删数据),排列的更加紧凑。非溢出桶无奈主动缩容,需手动解决缩容:创立一个新的 map 并从旧的 map 中复制元素。

Sync.map 构造:外部有两个 map,一个 read、一个 dirty,读时先无锁读 read,当 read 里没有时上锁读 dirty

Sync.Pool 原理:每个 P 创立一个本地对象池 poolLocal,尽量减少并发抵触;每个 poolLocal 都有一个 private 对象,优先存取 private 对象,防止简单逻辑;在 Get 和 Put 期间,利用 pin 锁定以后 P,避免 G 被抢占,造成程序凌乱。获取对象期间,利用对象窃取机制,从其余 P 的本地对象池以及 victim 中获取对象。充分利用 CPU Cache 个性,晋升程序性能。

golang Select 原理:https://www.codenong.com/cs106626574/

在一个 select 中,所有 case 语句会形成一个 scase 构造体的数组,Select 和 rselect 都是做了简略的初始化参数 selectgo 才是外围。selectgo 的做的事件是:

1、打乱传入的 case 构造体数组程序(select 的随机读写 channel 的实现形式),而后锁住其中的所有的 channel。

2、遍历所有的 channel,查看其是否可读或者可写。

3、其中的 channel 可读或者可写,则解锁所有 channel,返回对应的 channel 数据,都没读写但有 default 语句,返回 default 语句对应的 scase 并解锁所有的 channel。

4、没有 channel 可读写,没有 default 语句,将以后运行的 groutine 阻塞,退出到以后所有 channel 的期待队列中去,而后解锁所有 channel,期待被唤醒。

5、有阻塞的 channel 读写 ready 了则唤醒,再次加锁所有 channel,遍历所有 channel 找到对应的 channel 和 G,唤醒 G,将 G 从其余 channel 的期待队列中移除。

golang 协程与 epoll: https://strikefreedom.top/go-netpoll-io-multiplexing-reactor

1、当调用 net.Listen 时会创立 listenfd 并设置成非阻塞 而后调用 epollCreate 初始化 epollfd 并把 listenfd 并封装成 pollDesc 并调用 epollCtl 注册进 epoll 里。2、当 Accept 产生 connfd 时,办法外部会把 connfd 封装成 pollDesc 并调用 epollCtl 注册进 epoll 里。3、当 Read 或 Write 时,因为是非阻塞 IO,当没有数据,没有 IO 事件产生时,会返回 EAGAIN 谬误,会调用 fd.pd.waitRead 办法,把以后 G 进行 goPark,挂在 pollDesc 构造体里的 rg 或 wg 字段上,期待 epollWait 调度。4、当 scheule()或 sysmon 进行调度时,会调用 netpoll 办法进行 epollWait 调用,获取 rdlist 里有 IO 数据的 fd 所对应的 pollDesc 对象,把 fd 里的数据写入 pollDesc 里 rg 或 wg 挂载的 G,而后返回 G 链表并注入进全局队列期待被 P 调度。

golang 类型元数据、接口与反射:

类型元数据:有内置类型元数据和自定义类型元数据,自定义类型由根底类型元数据和 uncommontype 一起形容。type _type struct


空接口:底层构造体为 eface, 构造体内容为 type *_type 和 data unsafe.Pointer

非空接口:底层构造做为 iface, 构造体内容为 tab *itab 和 data unsafe.Pointer  


反射 TypeOf 与 ValueOf 性能差别的起因:TypeOf 操作全程是在栈上进行的。ValueOf 变量:会把变量显示地拷贝一份并逃逸到堆上留下拷贝变量的指针、ValueOf 变量指针:会把原变量逃逸到堆上并留下原变量的指针。ValueOf 减少了堆调配开销和 GC 开销。ValueOf 里有 rType 指针,能够操作 Type 接口办法。

golang 内存治理:https://www.php.cn/be/go/436885.html https://studygolang.com/articles/21025

struct{}不占用内存,编译器在内存调配时做的优化,当 mallocgc 发现对象 size 为 0 时,间接返回变量 zerobase 的援用,其是所有 0 字节的基准地址,不占据任何宽度。Golang 内存治理(mcache(治理 P 本地的 mspan 数组)、mcentral(治理全局 mspan)、每个 arena64M(8192 个 page)、mheap(单位 page 8KB 共 512G),mheap 拆分成多个 arena(64M),arena 又拆分成多个 span,heapArena 有个 spans 数组对应不同的 mspan,每个 P 都有独立的 mcache 缩小锁开销,mspan 外面依照 8 *2ⁿ大小的 object(8b,16b,32b ....)分为 67 种内存块,映射到对应类型的 span。有多少种类型的 mspan 就有多少个 mcentral。每个 mcentral 都会蕴含两个 mspan 的列表:没有闲暇对象或 mspan 曾经被 mcache 缓存的 mspan 列表(noempty mspanList),有闲暇对象的 mspan 列表(empty mspanList);因为 mspan 是全局的,会被所有的 mcache 拜访,所以会呈现并发性问题,因此 mcentral 会存在一个锁。mheap 有一个 mcentral 数组治理 136 个不同类型 mspan 的 mcentral 数组。mheap 治理整个 512G 的堆内存,heapArena 治理一个 64M 的 arena,heapArena 有一个 spans 数组字段,用于询址,寄存 span 元数据,mspan 治理一个 span(一组 pages)。bitmap 区域用于示意 arena 区域中哪些地址保留了对象, 并且对象中哪些地址蕴含了指针。bitmap 区域中一个字节对应了 arena 区域中的四个指针大小的内存, 即 2bit 对应一个指针大小的内存,标识需不需要在扫描,就是垃圾回收用的。Go 没法应用 P 的本地缓存 mcache 和全局核心缓存 mcentral 上超过 32KB 的内存调配,大于 32KB 内存间接申请 mheap,mcentral 内存不够时向 mheap 申请若干 page 并把 page 切割成 mspan 单位大小。对于小内存对象,节俭内存开销,打消内存碎片。mspan:spanclass 0 号标记为大于 32K 的内存,1-67 号为 8B-32KB 的内存。spanclass 最低位来标识是否要 GC 扫描:蕴含指针要 GC 的扫描的归为 scannable 这一类(0),不含指针的归为 noscan 这一类(1),一共分成 136 类。noscan 是不蕴含指针的对象,没有子对象,GC 实现 root 标记后不须要再对指针进行扫描。scan 则代表含有指针,标记完 root 后须要遍历其指针扫描。把 span 分为 scan 和 noscan 的意义在于,GC 扫描对象的时候对于 noscan 的 span 能够不去查看 bitmap 区域来标记子对象, 大幅晋升标记的效率。mspan 里 allocBits 位图字段:用于标记 mspan 里的哪些内存块被调配了。gcmarkBits 字段是用于 GC 标记阶段会对这个位图进行标记,看 span 里的内存块是否开释。到 GC 打扫阶段开释掉旧的 allocBits,把标记的 gcmarkBits 用作 allocBits , 而后给个清 0 的给 gcmarkBits,这个操作就是内存清理。freeIndex 记录着下个闲暇的内存索引块。零 GC 缓存原理,在 GC 时 scan 区标记完 root 后须要遍历其指针扫描,noscan 区实现 root 标记后不须要再被 GC 扫描标记,从而进步 GC 扫描性能。

golang 内存调配:

mallocgc 流程:1、辅助 GC;2、空间调配(三种内存调配策略:tiny 对象(小于 16B 且无指针)、mcache.alloc、large span);3、位图标记:用于线性地址找到对应的 span 内存块;4、收尾(GC 时新对象要标记,以及是否达到阀值要触发新一轮 GC)

GC:主体并发增量式回收 https://golang.design/under-the-hood/zh-cn/part2runtime/ch08g…

Go 语言引入并发垃圾收集器的同时应用垃圾收集调步 (Pacing) 算法计算触发的垃圾收集的最佳工夫,确保触发工夫不会节约计算资源,也不会超出预期的堆大小。标记打扫算法实现起来绝对简略,但容易造成碎片化,则有了 mcache 这种多级缓存模式。debug.SetGCPercent:用于设置一个垃圾收集比率 设置 GOGC 值,默认是 100。新分配内存 (本次堆内存的增量) 和前次垃圾收集剩下的存活数据的比率达到该百分比时,就会触发垃圾收集。默认 GOGC=100,增长率 100%。比方 1M 到 2M 到前面 500M 要到 1G 才会触发,当存活对象越多时越难达到阈值,所以要 2 分钟定时 GC。go 的 GC 策略,每次回收都会造成 CPU 的抖动,因 GC 应用 cpu 工夫约为总工夫的 25%,咱们设置 GOPROC 为 4,则每次标记时会占用 1 个 CPU 的 100% 工夫,直到 GC 解决实现。GC 时会有两种后台任务, 在 GC 初始化的筹备阶段会为每个 P 创立了 markWorker, 打扫工作只有一个后盾 sweeper 增量进行.GCMark 阶段后台任务会在须要时启动, 同时工作的 markWorker 数量大概是 GOMAXPROCS 的 25%.   后盾扫描标记内存对象时 CPU 默认利用率是 25%. 调度时启动 markWorker 数量为 P *25%。发现内存增长特地快,标记过程太长了,对象增长太快。会限定你 G 的并行度,规范是 25%,当初增长到了 50%。这样程序会跑的慢吞吐降落了。会多了个 MA(MarkAssistant)协程:标记协助者,帮助 GC 一块进行减速标记。若 3 核或者 6 核无奈被 4 整除,这时须要 1 个 G 或额定 1 个 G 的局部 CPU 帮助解决垃圾收集,运行时须要占用某个 CPU 的局部工夫, 每实现一定量工作时会计算是否达到 fractionalUtilizationGoal, 如果达到了 FractionalWoker 就能够让出 cpu 给其余协程用。应用 Fractional 模式的协程帮助保障 CPU 利用率达到 25%。GC 触发机会:手动调用,runtime.GC、sysmon,监控线程 2 分钟调用、runtime.mallocgc(),申请内存时判断堆大小是否达到阈值(新分配内存比上次 GC 存活数据达到 GOGC 比率)


优化 GC 频率:优化内存申请速度,尽可能少申请内存,复用已申请的内存。管制(协程的数量)、缩小(缩小内存申请次数,缩小 slice map 动静扩容)、复用(sync.Pool 缓存复用长期对象)、内存对齐(按占字节最大的类型来做对齐边界,压缩内存占用空间),尽可能地管制堆内存增长率从而缩小 GC 启动。或调整 GOGC 参数的值,增大触发阈值。

GC 流程:写屏障只有在标记阶段启用 https://golang.design/under-the-hood/zh-cn/part2runtime/ch08gc/

GC 标记时会 stopTheWorld 启动混合写屏障(栈空间不启动,堆空间启动),而后 startTheWord。再而后 ` 标记工作 ` 要从数据段和协程栈的 root 节点开始退出队列而后追踪到堆上的节点. 使其变黑并始终放弃,过程不须要 STW. 实现标记后会 stop the world 敞开混合写屏障。GC 标记阶段堆栈上新创建的对象都要标记成彩色。强三色不变性:彩色对象援用的必须是灰色,弱三色不变性:所有被彩色对象援用的红色对象都有被灰色援用或间接援用.

GCMark 标记筹备阶段,为并发标记做筹备工作,启动混合写屏障 赋值器处于 STW 状态

GCMarK 扫描标记阶段,与赋值器并发执行,赋值器处于 并发状态 占用 GOMAXPROCS 的 25% 的 gcBgMarkWorker

GCMarkTermination 标记终止阶段,保障一个周期内标记工作实现,敞开混合写屏障 赋值器这时处于 STW 状态

GCoff 内存打扫阶段,将须要回收的内存偿还到堆中,写屏障敞开 赋值器处于并发状态,GC 没有运行 sweep 在后盾运行,写屏障没有启用

GCoff 内存偿还阶段,将过多的内存归还给操作系统,写屏障敞开 赋值器处于并发状态,GC 没有运行 sweep 在后盾运行,写屏障没有启用

内存逃逸(逃逸剖析):https://segmentfault.com/a/1190000040450335

golang 逃逸剖析指令:go build -gcflags '-m'

栈对象逃逸到堆上,少逃逸是为了缩小对象在堆上调配,升高堆内存增长率,缩小 GC,从而缩小没必要的 GC 占用 CPU 利用率。内部援用 (指针逃逸)、动静类型逃逸(理解 interface{} 的内部结构后,能够归并到指针逃逸)、栈空间有余逃逸(对象太大)、闭包援用对象逃逸、slice 扩容逃逸,初始化时是调配在栈上的,运行时扩容会逃到堆上(无奈确定栈空间而逃逸)。

golang 协程栈空间:用的是间断栈

每个 G 保护着本人的栈区,它只能本人用不能被其余 G 用。G 运行时栈区会按需增长和膨胀,初始值是 2KB(线程 2M),不够时是成倍增长,栈空间最大限度在 64 位零碎上是 1GB,32 位零碎是 250MB。协程栈空间也是从堆内存里调配的,有全局缓存和本地缓存。栈增长:间断栈其外围原理是每当程序的栈空间有余要栈增长时,开拓一片更大的栈空间 (newstack) 并将原栈中的所有值都迁徙 copystack 到新栈中,旧栈则 stackfree 回收掉,新的局部变量或者函数调用就有短缺的内存空间,栈增长是成倍扩容的。栈膨胀:运行时栈内存应用有余 1 / 4 时会缩容(shrinkstack),会调用开拓新的栈空间并 runtime.copystack 缩容, 大小是原始栈的一半, 若新栈大小低于 G 最低限度 2KB, 缩容过程就会进行,GC 时发动栈膨胀。新栈的初始化和数据复制是一个简略的过程,但这不是整个过程中最简单的中央,还要将指向源栈中的内存指向新栈。所有指针都被调整后,通过 runtime.stackfree 开释旧栈的内存空间。

golang 协程切换的机会:

1、select 阻塞时,2、io(文件,网络)读写阻塞时,3、channel 阻塞时,4、加锁 / 期待锁,5、sleep 时,6、GC 时:GC 的 gcBgMarkWoker 会抢占 P,7、零碎调用和抢占时 sysmon 线程会监控并把他们调度走,8、runtime.Gosched()。这些阻塞会调用 gopark 把协程切换到对应的构造体里挂起,当就绪时 goready 会把他们扔回 P 的本地队列期待调度。零碎调用工夫过长时会切出去独立线程解决。

golang cpu 利用率:死循环或过程有大量计算、协程、GC 在执行垃圾回收时会占用所有 cpu 的 25%。负载高: 还是零碎调用导致线程数多,工作数过多。

P 的最大下限:Go1.10 开始,P 没有限度,运行时不限度 GOMAXPROCS,之前限度为 1024 当初是 int32 的最大值,但也受内存限度。切片 allp 保留全副 p。

panic 触发场景:数组 / 切片越界、空指针异样、类型断言不匹配、除 0、向已敞开 channel 发消息、反复敞开 channel、敞开未初始化的 channel、拜访未初始化 map、sync 计数正数、过早敞开 HTTP 响应体

本文由 mdnice 多平台公布

正文完
 0