go语言GC面试-创新互联
垃圾回收也称为GC(Garbage Collection),是一种自动内存管理机制
让客户满意是我们工作的目标,不断超越客户的期望值来自于我们对这个行业的热爱。我们立志把好的技术通过有效、简单的方式提供给客户,将通过不懈努力成为客户在信息化领域值得信任、有价值的长期合作伙伴,公司提供的服务项目有:域名注册、网页空间、营销软件、网站建设、长顺网站维护、网站推广。现代高级编程语言管理内存的方式分为两种:自动和手动,像C、C++ 等编程语言使用手动管理内存的方式,工程师编写代码过程中需要主动申请或者释放内存;而 PHP、Java 和 Go 等语言使用自动的内存管理系统,有内存分配器和垃圾收集器来代为分配和回收内存,其中垃圾收集器就是我们常说的GC。
在应用程序中会使用到两种内存,分别为堆(Heap)和栈(Stack),GC负责回收堆内存,而不负责回收栈中的内存:
栈是线程的专用内存,专门为了函数执行而准备的,存储着函数中的局部变量以及调用栈,函数执行完后,编译器可以将栈上分配的内存可以直接释放,不需要通过GC来回收。
堆是程序共享的内存,需要GC进行回收在堆上分配的内存。
垃圾回收器的执行过程被划分为两个半独立的组件:
- 赋值器(Mutator):这一名称本质上是在指代用户态的代码。因为对垃圾回收器而言,用户态的代码仅仅只是在修改对象之间的引用关系,也就是在对象图(对象之间引用关系的一个有向图)上进行操作。
- 回收器(Collector):负责执行垃圾回收的代码。
常见的垃圾回收算法有以下几种:
- **引用计数:**对每个对象维护一个引用计数,当引用该对象的对象被销毁时,引用计数减1,当引用计数器为0时回收该对象。
优点:对象可以很快的被回收,不会出现内存耗尽或达到某个阀值时才回收。
缺点:不能很好的处理循环引用,而且实时维护引用计数,有也一定的代价。
代表语言:Python、PHP - **标记-清除:**从根变量开始遍历所有引用的对象,引用的对象标记为"被引用",没有被标记的进行回收。
优点:解决了引用计数的缺点。
缺点:需要STW,即要暂时停掉程序运行。
代表语言:Golang(其采用三色标记法) - **分代收集:**按照对象生命周期长短划分不同的代空间,生命周期长的放入老年代,而短的放入新生代,不同代有不能的回收算法和回收频率。
优点:回收性能好
缺点:算法复杂
代表语言: JAVA
此算法主要有两个主要的步骤:
标记(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一旦开始,无论是创建对象还是对象的引用改变,都会先变为灰色。
GC 的触发情况主要分为两大类,分别是:
- 系统触发:运行时自行根据内置的条件,检查、发现到,则进行 GC 处理,维护整个应用程序的可用性。
- 使用系统监控,该触发条件由
runtime.forcegcperiod
变量控制,默认为 2 分 钟。当超过两分钟没有产生任何 GC 时,强制触发 GC。 - b.使用步调(Pacing)算法,根据内存分配阈值触发,该触发条件由环境变量GOGC控制,默认值为100(100%),当前堆内存占用是上次GC结束后占用内存的2倍时,触发GC
- 使用系统监控,该触发条件由
- 手动触发:开发者在业务代码中自行调用 runtime.GC 方法来触发 GC 行为。
一次完整的垃圾回收会分为四个阶段,分别是标记准备、标记开始、标记终止、清理:
- 标记准备(Mark Setup):打开写屏障(Write Barrier),需 STW(stop the world)
- 标记开始(Marking):使用三色标记法并发标记 ,与用户程序并发执行
- 标记终止(Mark Termination):对触发写屏障的对象进行重新扫描标记,关闭写屏障(Write Barrier),需 STW(stop the world)
- 清理(Sweeping):将需要回收的内存归还到堆中,将过多的内存归还给操作系统,与用户程序并发执行
Go1.14 版本以 STW 为界限,可以将 GC 划分为五个阶段:
- GCMark 标记准备阶段,为并发标记做准备工作,启动写屏障
- STWGCMark 扫描标记阶段,与赋值器并发执行,写屏障开启并发
- GCMarkTermination 标记终止阶段,保证一个周期内标记任务完成,停止写屏障
- GCoff 内存清扫阶段,将需要回收的内存归还到堆中,写屏障关闭
- GCoff 内存归还阶段,将过多的内存归还给操作系统,写屏障关闭。
通过 go tool pprof 和 go tool trace 等工具
- 需要时,增大 GOGC 的值,降低 GC 的运行频率。
- 控制内存分配的速度,限制 Goroutine 的数量,提高赋值器 mutator 的 CPU 利用率(降低GC的CPU利用率)
- 少量使用
+
连接string - slice提前分配足够的内存来降低扩容带来的拷贝
- 避免map key对象过多,导致扫描时间增加
- 变量复用,减少对象分配,例如使用 sync.Pool 来复用需要频繁创建临时对象、使用全局变量等例 。如提前分配足够的内存来降低多余的拷贝。
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 | 标记结束时,堆的大小的预测值 |
8 | P 的数量 |
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
打开浏览器后,可以看到如下统计:
点击View trace,可以查看当时的trace情况
点击 Minimum mutator utilization,可以查看到赋值器 mutator (用户程序)对 CPU 的利用率 74.1%,接近100%则代表没有针对GC的优化空间了
3. debug.ReadGCStatspackage 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
字段含义由下表所示:
字段 | 含义 |
---|---|
NumGC | GC总次数 |
LastGC | 上次GC时间 |
PauseTotal | STW总耗时 |
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
字段含义由下表所示:
字段 | 含义 |
---|---|
NumGC | GC总次数 |
LastGC | 上次GC时间 |
HeapObjects | 堆中已经分配的对象总数,GC内存回收后HeapObjects取值相应减小 |
HeapAlloc | 堆中已经分配给对象的字节数,GC内存回收后HeapAlloc取值相应减小 |
NextGC | 下次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