go语言GC面试-创新互联

一、go语言GC原理剖析 1.1 GC介绍

垃圾回收也称为GC(Garbage Collection),是一种自动内存管理机制

让客户满意是我们工作的目标,不断超越客户的期望值来自于我们对这个行业的热爱。我们立志把好的技术通过有效、简单的方式提供给客户,将通过不懈努力成为客户在信息化领域值得信任、有价值的长期合作伙伴,公司提供的服务项目有:域名注册、网页空间、营销软件、网站建设、长顺网站维护、网站推广。

现代高级编程语言管理内存的方式分为两种:自动和手动,像C、C++ 等编程语言使用手动管理内存的方式,工程师编写代码过程中需要主动申请或者释放内存;而 PHP、Java 和 Go 等语言使用自动的内存管理系统,有内存分配器和垃圾收集器来代为分配和回收内存,其中垃圾收集器就是我们常说的GC。

在应用程序中会使用到两种内存,分别为堆(Heap)和栈(Stack),GC负责回收堆内存,而不负责回收栈中的内存:

栈是线程的专用内存,专门为了函数执行而准备的,存储着函数中的局部变量以及调用栈,函数执行完后,编译器可以将栈上分配的内存可以直接释放,不需要通过GC来回收。

堆是程序共享的内存,需要GC进行回收在堆上分配的内存。

垃圾回收器的执行过程被划分为两个半独立的组件:

  • 赋值器(Mutator):这一名称本质上是在指代用户态的代码。因为对垃圾回收器而言,用户态的代码仅仅只是在修改对象之间的引用关系,也就是在对象图(对象之间引用关系的一个有向图)上进行操作。
  • 回收器(Collector):负责执行垃圾回收的代码。
1.2 常用垃圾回收算法

常见的垃圾回收算法有以下几种:

  • **引用计数:**对每个对象维护一个引用计数,当引用该对象的对象被销毁时,引用计数减1,当引用计数器为0时回收该对象。
    优点:对象可以很快的被回收,不会出现内存耗尽或达到某个阀值时才回收。
    缺点:不能很好的处理循环引用,而且实时维护引用计数,有也一定的代价。
    代表语言:Python、PHP
  • **标记-清除:**从根变量开始遍历所有引用的对象,引用的对象标记为"被引用",没有被标记的进行回收。
    优点:解决了引用计数的缺点。
    缺点:需要STW,即要暂时停掉程序运行。
    代表语言:Golang(其采用三色标记法)
  • **分代收集:**按照对象生命周期长短划分不同的代空间,生命周期长的放入老年代,而短的放入新生代,不同代有不能的回收算法和回收频率。
    优点:回收性能好
    缺点:算法复杂
    代表语言: JAVA
1.3 go语言采用的垃圾回收机制 标记清除:

此算法主要有两个主要的步骤:

标记(Mark phase)

清除(Sweep phase)

第一步,找出不可达的对象,然后做上标记。
第二步,回收标记好的对象。

操作非常简单,但是有一点需要额外注意:mark and sweep算法在执行的时候,需要程序暂停!即 stop the world。
也就是说,这段时间程序会卡在哪儿。故中文翻译成 卡顿.

标记-清扫(Mark And Sweep)算法存在什么问题?
标记-清扫(Mark And Sweep)算法这种算法虽然非常的简单,但是还存在一些问题:

STW,stop the world;让程序暂停,程序出现卡顿。

标记需要扫描整个heap

清除数据会产生heap碎片
这里面最重要的问题就是:mark-and-sweep 算法会暂停整个程序。

三色标记法

此算法是在Go 1.5版本开始使用,Go 语言采用的是标记清除算法,并在此基础上使用了三色标记法和混合写屏障技术,GC过程和其他用户goroutine可并发运行,但需要一定时间的STW

三色标记法只是为了叙述方便而抽象出来的一种说法,实际上的对象是没有三色之分的。这里的三色,对应了垃圾回收过程中对象的三种状态:

  • 灰色:对象还在标记队列中等待
  • 黑色:对象已被标记,gcmarkBits对应位为1(该对象不会在本次 GC 中被回收)
  • 白色:对象未被标记,gcmarkBits对应位为0(该对象将会在本次 GC 中被清理)

step 1: 创建:白、灰、黑 三个集合

step 2: 将所有对象放入白色集合中

step 3: 遍历所有root对象,把遍历到的对象从白色集合放入灰色集合 (这里放入灰色集合的都是根节点的对象)

step 4: 遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,自身标记为黑色

step 5: 重复步骤4,直到灰色中无任何对象,其中用到2个机制:

  • 写屏障(Write Barrier):上面说到的 STW 的目的是防止 GC 扫描时内存变化引起的混乱,而写屏障就是让 goroutine 与 GC 同时运行的手段,虽然不能完全消除 STW,但是可以大大减少 STW 的时间。写屏障在 GC 的特定时间开启,开启后指针传递时会把指针标记,即本轮不回收,下次 GC 时再确定。
  • 辅助 GC(Mutator Assist):为了防止内存分配过快,在 GC 执行过程中,GC 过程中 mutator 线程会并发运行,而 mutator assist 机制会协助 GC 做一部分的工作。

step 6: 收集所有白色对象(垃圾)

root对象

根对象在垃圾回收的术语中又叫做根集合,它是垃圾回收器在标记过程时最先检查的对象,包括:

全局变量:程序在编译期就能确定的那些存在于程序整个生命周期的变量。
执行栈:每个 goroutine 都包含自己的执行栈,这些执行栈上指向堆内存的指针。
寄存器:寄存器的值可能表示一个指针,参与计算的这些指针可能指向某些赋值器分配的堆内存区块。

1)初始状态下所有对象都是白色的。
2)从根节点开始遍历所有对象,把遍历到的对象变成灰色对象
3)遍历灰色对象,将灰色对象引用的对象也变成灰色,然后将遍历过的灰色对象变成黑色对象。
4)循环步骤3,直到灰色对象全部变黑色。
5)回收所有白色对象(垃圾)。

6)最后,将所有黑色对象变为白色,并重复以上所有过程。

当三色的标记清除的标记阶段结束之后,应用程序的堆中就不存在任何的灰色对象,我们只能看到黑色的存活对象以及白色的垃圾对象,垃圾收集器可以回收这些白色的垃圾,

混合写屏障: 插入写屏障

对象被引用时触发的机制(只在堆内存中生效):赋值器这一行为通知给并发执行的回收器,被引用的对象标记为灰色

缺点:结束时需要STW来重新扫描栈,标记栈上引用的白色对象的存活

删除写屏障

对象被删除时触发的机制(只在堆内存中生效):赋值器将这一行为通知给并发执行的回收器,被删除的对象,如果自身为灰色或者白色,那么标记为灰色

缺点:一个对象的引用被删除后,即使没有其他存活的对象引用它,它仍然会活到下一轮,会产生很大冗余扫描成本,且降低了回收精度

混合写屏障

GC没有混合写屏障前,一直是插入写屏障;混合写屏障是插入写屏障 + 删除写屏障,写屏障只应用在堆上应用,栈上不启用(栈上启用成本很高)

  • GC开始将栈上的对象全部扫描并标记为黑色。
  • GC期间,任何在栈上创建的新对象,均为黑色。
  • 被删除的对象标记为灰色。
  • 被添加的对象标记为灰色。

插入写屏障:对象A引用C,A黑C白,会把C加入写屏障buf,最终flush到扫描队列。
删除屏障:被删除的对象,如果自身为灰色或者白色,那么被标记为灰色。(保护灰色到白色的路径不会断)。
插⼊写屏障和删除写屏障的短板:
插⼊写屏障:结束时需要STW来重新扫描栈,标记栈上引⽤的⽩⾊对象的存活;
删除写屏障:回收精度低,GC开始时STW扫描堆栈来记录初始快照,这个过程会保护开始时刻的所有存活对象。

注意:
当gc进行中时,新创建一个对象. 按照三色标记法的步骤,对象会被标记为白色,这样新生成的对象最后会被清除掉,这样会影响程序逻辑.
golang引入写屏障机制.可以监控对象的内存修改,并对对象进行重新标记.
gc一旦开始,无论是创建对象还是对象的引用改变,都会先变为灰色。

1.4 GC触发

GC 的触发情况主要分为两大类,分别是:

  1. 系统触发:运行时自行根据内置的条件,检查、发现到,则进行 GC 处理,维护整个应用程序的可用性。
    • 使用系统监控,该触发条件由runtime.forcegcperiod变量控制,默认为 2 分 钟。当超过两分钟没有产生任何 GC 时,强制触发 GC。
    • b.使用步调(Pacing)算法,根据内存分配阈值触发,该触发条件由环境变量GOGC控制,默认值为100(100%),当前堆内存占用是上次GC结束后占用内存的2倍时,触发GC
  2. 手动触发:开发者在业务代码中自行调用 runtime.GC 方法来触发 GC 行为。
1.5 GC流程

一次完整的垃圾回收会分为四个阶段,分别是标记准备、标记开始、标记终止、清理:

  1. 标记准备(Mark Setup):打开写屏障(Write Barrier),需 STW(stop the world)
  2. 标记开始(Marking):使用三色标记法并发标记 ,与用户程序并发执行
  3. 标记终止(Mark Termination):对触发写屏障的对象进行重新扫描标记,关闭写屏障(Write Barrier),需 STW(stop the world)
  4. 清理(Sweeping):将需要回收的内存归还到堆中,将过多的内存归还给操作系统,与用户程序并发执行

img

Go1.14 版本以 STW 为界限,可以将 GC 划分为五个阶段:

  1. GCMark 标记准备阶段,为并发标记做准备工作,启动写屏障
  2. STWGCMark 扫描标记阶段,与赋值器并发执行,写屏障开启并发
  3. GCMarkTermination 标记终止阶段,保证一个周期内标记任务完成,停止写屏障
  4. GCoff 内存清扫阶段,将需要回收的内存归还到堆中,写屏障关闭
  5. GCoff 内存归还阶段,将过多的内存归还给操作系统,写屏障关闭。
1.6 GC调优

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

  • 需要时,增大 GOGC 的值,降低 GC 的运行频率。
  • 控制内存分配的速度,限制 Goroutine 的数量,提高赋值器 mutator 的 CPU 利用率(降低GC的CPU利用率)
  • 少量使用+连接string
  • slice提前分配足够的内存来降低扩容带来的拷贝
  • 避免map key对象过多,导致扫描时间增加
  • 变量复用,减少对象分配,例如使用 sync.Pool 来复用需要频繁创建临时对象、使用全局变量等例 。如提前分配足够的内存来降低多余的拷贝。
1.7 查看GC历史 1. GODEBUG=‘gctrace=1’
package main
func main() {
for n := 1; n< 100000; n++ {
_ = make([]byte, 1<<20)
}
}
$ GODEBUG='gctrace=1' go run main.go

gc 1 @0.003s 4%: 0.013+1.7+0.008 ms clock, 0.10+0.67/1.2/0.018+0.064 ms cpu, 4->6->2 MB, 5 MB goal, 8 P
gc 2 @0.006s 2%: 0.006+4.5+0.058 ms clock, 0.048+0.070/0.027/3.6+0.47 ms cpu, 4->5->1 MB, 5 MB goal, 8 P
gc 3 @0.011s 3%: 0.021+1.3+0.009 ms clock, 0.17+0.041/0.41/0.046+0.072 ms cpu, 4->6->2 MB, 5 MB goal, 8 P
gc 4 @0.013s 5%: 0.025+0.38+0.26 ms clock, 0.20+0.054/0.15/0.009+2.1 ms cpu, 4->6->2 MB, 5 MB goal, 8 P
gc 5 @0.014s 5%: 0.021+0.16+0.002 ms clock, 0.17+0.098/0.028/0.001+0.016 ms cpu, 4->5->1 MB, 5 MB goal, 8 P
gc 6 @0.014s 7%: 0.025+1.6+0.003 ms clock, 0.20+0.061/2.9/1.5+0.025 ms cpu, 4->6->2 MB, 5 MB goal, 8 P
gc 7 @0.016s 7%: 0.019+1.0+0.002 ms clock, 0.15+0.053/1.0/0.018+0.017 ms cpu, 4->6->2 MB, 5 MB goal, 8 P
gc 8 @0.017s 7%: 0.029+0.17+0.002 ms clock, 0.23+0.037/0.10/0.063+0.022 ms cpu, 4->4->0 MB, 5 MB goal, 8 P
gc 9 @0.018s 7%: 0.019+0.23+0.002 ms clock, 0.15+0.040/0.16/0.023+0.018 ms cpu, 4->5->1 MB, 5 MB goal, 8 P
gc 10 @0.018s 7%: 0.022+0.23+0.003 ms clock, 0.17+0.061/0.13/0.006+0.024 ms cpu, 4->6->2 MB, 5 MB goal, 8 P
gc 11 @0.018s 7%: 0.019+0.11+0.001 ms clock, 0.15+0.033/0.051/0.013+0.015 ms cpu, 4->5->1 MB, 5 MB goal, 8 P
gc 12 @0.019s 7%: 0.018+0.19+0.001 ms clock, 0.14+0.035/0.10/0.018+0.014 ms cpu, 4->5->1 MB, 5 MB goal, 8 P
gc 13 @0.019s 7%: 0.018+0.35+0.002 ms clock, 0.15+0.21/0.054/0.013+0.016 ms cpu, 4->5->1 MB, 5 MB goal, 8 P
gc 14 @0.019s 8%: 0.024+0.27+0.002 ms clock, 0.19+0.022/0.13/0.014+0.017 ms cpu, 4->5->1 MB, 5 MB goal, 8 P
gc 15 @0.020s 8%: 0.019+0.42+0.038 ms clock, 0.15+0.060/0.28/0.007+0.31 ms cpu, 4->17->13 MB, 5 MB goal, 8 P
gc 16 @0.021s 8%: 0.018+0.53+0.060 ms clock, 0.14+0.045/0.39/0.005+0.48 ms cpu, 21->28->7 MB, 26 MB goal, 8 P
gc 17 @0.021s 10%: 0.020+0.91+0.64 ms clock, 0.16+0.050/0.36/0.027+5.1 ms cpu, 12->16->4 MB, 14 MB goal, 8 P
gc 18 @0.023s 10%: 0.020+0.55+0.002 ms clock, 0.16+0.053/0.50/0.081+0.023 ms cpu, 7->9->2 MB, 8 MB goal, 8 P

字段含义由下表所示:

字段含义
gc 2第二个 GC 周期
0.006程序开始后的 0.006 秒
2%该 GC 周期中 CPU 的使用率
0.006标记开始时, STW 所花费的时间(wall clock)
4.5标记过程中,并发标记所花费的时间(wall clock)
0.058标记终止时, STW 所花费的时间(wall clock)
0.048标记开始时, STW 所花费的时间(cpu time)
0.070标记过程中,标记辅助所花费的时间(cpu time)
0.027标记过程中,并发标记所花费的时间(cpu time)
3.6标记过程中,GC 空闲的时间(cpu time)
0.47标记终止时, STW 所花费的时间(cpu time)
4标记开始时,堆的大小的实际值
5标记结束时,堆的大小的实际值
1标记结束时,标记为存活的对象大小
5标记结束时,堆的大小的预测值
8P 的数量
2. go tool trace
package main

import (
"os"
"runtime/trace"
)

func main() {
f, _ := os.Create("trace.out")
defer f.Close()
trace.Start(f)
defer trace.Stop()
for n := 1; n< 100000; n++ {
_ = make([]byte, 1<<20)
}
}
$ go run main.go
$ go tool trace trace.out

打开浏览器后,可以看到如下统计:

img

点击View trace,可以查看当时的trace情况

img

点击 Minimum mutator utilization,可以查看到赋值器 mutator (用户程序)对 CPU 的利用率 74.1%,接近100%则代表没有针对GC的优化空间了

img

3. debug.ReadGCStats
package main

import (
"fmt"
"runtime/debug"
"time"
)

func printGCStats() {
t := time.NewTicker(time.Second)
s := debug.GCStats{}
for {
select {
case<-t.C:
debug.ReadGCStats(&s)
fmt.Printf("gc %d last@%v, PauseTotal %v\n", s.NumGC, s.LastGC, s.PauseTotal)
}
}
}
func main() {
go printGCStats()
for n := 1; n< 100000; n++ {
_ = make([]byte, 1<<20)
}
}
$ go run main.go

gc 3392 last@2022-05-04 19:22:52.877293 +0800 CST, PauseTotal 117.524907ms
gc 6591 last@2022-05-04 19:22:53.876837 +0800 CST, PauseTotal 253.254996ms
gc 10028 last@2022-05-04 19:22:54.87674 +0800 CST, PauseTotal 376.981595ms
gc 13447 last@2022-05-04 19:22:55.87689 +0800 CST, PauseTotal 511.420111ms
gc 16938 last@2022-05-04 19:22:56.876955 +0800 CST, PauseTotal 649.293449ms
gc 20350 last@2022-05-04 19:22:57.876756 +0800 CST, PauseTotal 788.003014ms

字段含义由下表所示:

字段含义
NumGCGC总次数
LastGC上次GC时间
PauseTotalSTW总耗时
4. runtime.ReadMemStats
package main

import (
"fmt"
"runtime"
"time"
)

func printMemStats() {
t := time.NewTicker(time.Second)
s := runtime.MemStats{}
for {
select {
case<-t.C:
runtime.ReadMemStats(&s)
fmt.Printf("gc %d last@%v, heap_object_num: %v, heap_alloc: %vMB, next_heap_size: %vMB\n",
s.NumGC, time.Unix(int64(time.Duration(s.LastGC).Seconds()), 0), s.HeapObjects, s.HeapAlloc/(1<<20), s.NextGC/(1<<20))
}
}
}
func main() {
go printMemStats()
fmt.Println(1<< 20)
for n := 1; n< 100000; n++ {
_ = make([]byte, 1<<20)
}
}
$ go run main.go

gc 2978 last@2022-05-04 19:38:04 +0800 CST, heap_object_num: 391, heap_alloc: 20MB, next_heap_size: 28MB
gc 5817 last@2022-05-04 19:38:05 +0800 CST, heap_object_num: 370, heap_alloc: 4MB, next_heap_size: 4MB
gc 9415 last@2022-05-04 19:38:06 +0800 CST, heap_object_num: 392, heap_alloc: 7MB, next_heap_size: 8MB
gc 11429 last@2022-05-04 19:38:07 +0800 CST, heap_object_num: 339, heap_alloc: 4MB, next_heap_size: 5MB
gc 14706 last@2022-05-04 19:38:08 +0800 CST, heap_object_num: 436, heap_alloc: 6MB, next_heap_size: 8MB
gc 18253 last@2022-05-04 19:38:09 +0800 CST, heap_object_num: 375, heap_alloc: 4MB, next_heap_size: 6M

字段含义由下表所示:

字段含义
NumGCGC总次数
LastGC上次GC时间
HeapObjects堆中已经分配的对象总数,GC内存回收后HeapObjects取值相应减小
HeapAlloc堆中已经分配给对象的字节数,GC内存回收后HeapAlloc取值相应减小
NextGC下次GC目标堆的大小
1.8 GC算法演进
  • Go 1:mark and sweep操作都需要STW
  • Go 1.3:分离了mark和sweep操作,mark过程需要 STW,mark完成后让sweep任务和普通协程任务一样并行,停顿时间在约几百ms
  • Go 1.5:引入三色并发标记法、插入写屏障,不需要每次都扫描整个内存空间,可以减少stop the world的时间,停顿时间在100ms以内
  • Go 1.6:使用 bitmap 来记录回收内存的位置,大幅优化垃圾回收器自身消耗的内存,停顿时间在10ms以内
  • Go 1.7:停顿时间控制在2ms以内
  • Go 1.8:混合写屏障(插入写屏障和删除写屏障),停顿时间在0.5ms左右
  • Go 1.9:彻底移除了栈的重扫描过程
  • Go 1.12:整合了两个阶段的 Mark Termination
  • Go 1.13:着手解决向操作系统归还内存的,提出了新的 Scavenger
    了mark和sweep操作,mark过程需要 STW,mark完成后让sweep任务和普通协程任务一样并行,停顿时间在约几百ms
  • Go 1.5:引入三色并发标记法、插入写屏障,不需要每次都扫描整个内存空间,可以减少stop the world的时间,停顿时间在100ms以内
  • Go 1.6:使用 bitmap 来记录回收内存的位置,大幅优化垃圾回收器自身消耗的内存,停顿时间在10ms以内
  • Go 1.7:停顿时间控制在2ms以内
  • Go 1.8:混合写屏障(插入写屏障和删除写屏障),停顿时间在0.5ms左右
  • Go 1.9:彻底移除了栈的重扫描过程
  • Go 1.12:整合了两个阶段的 Mark Termination
  • Go 1.13:着手解决向操作系统归还内存的,提出了新的 Scavenger
  • Go 1.14:替代了仅存活了一个版本的 scavenger,全新的页分配器,优化分配内存过程的速率与现有的扩展性问题,并引入了异步抢占,解决了由于密集循环导致的 STW 时间过长的问题

你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧


网页名称:go语言GC面试-创新互联
新闻来源:http://cdiso.cn/article/cseois.html

其他资讯