每位 Gopher 都应该了解的 Golang 语言的垃圾回收算法

简介: 每位 Gopher 都应该了解的 Golang 语言的垃圾回收算法

01

介绍


关于垃圾回收,比较常见的算法有引用计数、标记清除和分代收集。Golang 语言使用的垃圾回收算法是标记清除。本文主要介绍一下 Golang 语言的垃圾回收算法。

02

Golang 语言 v1.3 及之前的垃圾回收 - 标记清除


Golang 语言的标记清除垃圾回收算法,为了防止 GC 扫描时内存变化引起的混乱,那么就需要 STW,即 Stop The World,具体在 Golang 语言中是指,在 GC 时,先停止所有 goroutine,再进行垃圾回收,等待垃圾回收结束后再恢复所有被停止的 goroutine。关于 STW 执行流程,可以参考下面这张经典图片。


640.jpg

「图片来自网络」

标记清除:

  1. 启动 STW,暂停程序的业务逻辑,找出不可达对象和可达对象。
  2. 将所有可达对象做标记。
  3. 清除未标记的对象。
  4. 停止 STW,程序继续执行。
  5. 循环往复,直到进程程序生命周期结束。

标记清除的缺点:

  1. STW 需要暂停程序,导致程序卡顿。
  2. 做标记需要扫描整个 heap(堆)。
  3. 清除数据会产生 head(堆)碎片。

标记清除的优化:

因为 STW 需要暂停程序,为了减少暂停程序的时间,将清除操作移出 STW 执行周期,但是优化效果不明显,进一步优化请继续阅读下文。

03

Golang 语言 v1.5 的垃圾回收 - 三色标记


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

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

三色标记:

  1. 新创建的对象,默认标记为白色。
  2. 从根节点开始遍历所有白色对象,将遍历到的对象的颜色由白色改为灰色。
  3. 将灰色对象作为根节点开始遍历所有白色对象,将遍历到的对象的颜色由白色改为灰色,并将作为根节点的灰色对象的颜色由灰色改为黑色。
  4. 循环往复,直到所有灰色对象的颜色都变为黑色。
  5. 将剩余的白色对象全部清除。

三色标记的缺点:

一个不被灰色对象可达的白色对象,如果被一个黑色对象引用,将会造成该白色对象丢失的问题。

三色标记的优化:

Golang 官方通过强/弱三色不变性,对三色标记做了优化。强三色不变性,即强制性不允许黑色对象引用白色对象;

弱三色不变性,即黑色对象可以引用白色对象,但是必须满足一个条件,该白色对象必须有灰色对象对它的直接引用,或者是可达链路中包含灰色对象。

具体实现是通过写屏障(Write Barrier),即在 GC 的特定时间开启,开启后指针传递时会把指针标记,被标记的指针在本次 GC 过程中不会被清理,等到下次 GC 时,才会被清理。写屏障的目的就是为了缩短 STW 的时间,让 goroutine 和 GC 同时运行。

Golang 语言中的写屏障分为插入写屏障和删除写屏障。

插入写屏障的含义:

满足强三色不变性,即被引用对象,会被强制标记为灰色。

插入写屏障的缺点:

结束时需要 STW 重新扫描栈,大约需要 10-100ms。

删除写屏障的含义:

满足弱三色不变性,即被删除对象,如果自身为灰色或者白色,会被标记为灰色。

删除写屏障的缺点:

回收精度低,即一个对象即使被删除了,最后一个指向该对象的指针也会等到下一次 GC 回收中才被清理。

04

Golang 语言 v1.8 的垃圾回收 - 混合写屏障


Golang 语言的团队为了更进一步优化垃圾回收,采用了混合写屏障。

混合写屏障:

  1. 后续无需 STW,GC 在首次执行时,先将栈上的所有对象都标记为黑色。
  2. GC 在执行过程中,在栈上新创建的对象,默认被标记为黑色。
  3. 将被删除的对象标记为灰色。
  4. 将被添加的对象标记为灰色。

混合写屏障的优点:

混合写屏障,满足弱三色不变性,结合了插入写屏障和删除写屏障的优点。

05

Golang 语言的 GC 触发方式

  1. 内存分配阈值,阈值=上次 GC 内存分配值 * 内存增长率,其中内存增长率由环境变量 GOGC 设定,默认值为 100。每次内存分配时,都会先检查当前内存分配是否已经达到阈值,如果已达到阈值,就会触发 GC,即每当内存分配量将要增长一倍时则触发 GC。
  2. 定时触发,src/runtime/proc.go 文件中的 forcegcperiod 设定触发 GC 的时间间隔,默认值为 2 分钟。
  3. 手动触发,通过调用 runtime.GC() 方法,触发 GC。

06

调式 GC


Golang 语言使用 GODEBUG 调式 GC: GODEBUG=gctrace=1 go run main.go

输出结果:

gc 1 @0.013s 0%: 0.037+0.36+0.004 ms clock, 0.60+0.48/0.81/0.012+0.073 ms cpu, 4->4->0 MB, 5 MB goal, 16 P
gc 2 @0.016s 1%: 0.010+0.24+0.004 ms clock, 0.17+0.29/0.44/0.21+0.064 ms cpu, 4->4->0 MB, 5 MB goal, 16 P
gc 3 @0.019s 1%: 0.069+0.50+0.041 ms clock, 1.1+0.29/0.68/0.13+0.66 ms cpu, 4->4->0 MB, 5 MB goal, 16 P
gc 4 @0.021s 2%: 0.056+0.35+0.041 ms clock, 0.90+0.33/0.67/0.064+0.65 ms cpu, 4->4->0 MB, 5 MB goal, 16 P
gc 5 @0.023s 2%: 0.053+0.27+0.003 ms clock, 0.85+0.42/0.63/0.069+0.057 ms cpu, 4->4->0 MB, 5 MB goal, 16 P

输出结果的含义:

  1. gc 1 @0.013s 0%: 表示第 1 次执行 GC,0.013s 表示执行时间。
  2. 0% GC 占用进程的进程 CPU 时间的百分比。
  3. 0.037+0.36+0.004 ms clock 表示 GC 耗时,依次是 STW 清扫的时间,并发标记和扫描的时间,STW 标记的时间,即 stop-the-world (STW) sweep termination + concurrent mark and scan + and STW mark termination
  4. 0.60+0.48/0.81/0.012+0.073 ms cpu GC 占用的 CPU 时间。
  5. 4->4->0 MB 依次表示堆的大小,GC 后堆的大小,存活堆的大小。
  6. 5 MB goal 表示整体堆的大小。
  7. 16 P 表示 CPU 的核心数。
  8. GC forced 表示调用 runtime.GC() 方法,手动执行 GC。

scvg0: inuse: 6, idle: 12, sys: 18, released: 0, consumed: 18 (MB)  
scvg0: inuse: 6, idle: 9, sys: 15, released: 0, consumed: 15 (MB)  
GC forced  

  • inuse:内存使用大小。
  • idle:需要清除的空闲内存。
  • sys: 系统映射的内存。
  • released:释放的系统内存。
  • consumed:申请的系统内存。

07

总结

本文通过 Golang 语言的 v1.3、v1.5 和 v1.8 三个版本的 Golang 语言的算法的演进介绍垃圾回收。实际上几乎每个版本都会涉及垃圾回收的优化,相关代码也越来越复杂。如果读者希望更深入了解垃圾回收相关的内容,建议阅读相关源码。

尽管 Golang 语言可以自动进行垃圾回收,但是 GC 也会消耗资源,尽量还是在编写 Golang 代码的时候减少对象分配的数量,采用对象复用、将小对象组合成大对象或采用精准的数据类型,比如可以使用 int8,绝不使用 int。还可以在编写 Golang 代码的时候,手动触发 GC,将不再使用的内存及时释放。




目录
相关文章
|
1月前
|
存储 人工智能 算法
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
这篇文章详细介绍了Dijkstra和Floyd算法,这两种算法分别用于解决单源和多源最短路径问题,并且提供了Java语言的实现代码。
73 3
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
|
2月前
|
Go
Golang语言之管道channel快速入门篇
这篇文章是关于Go语言中管道(channel)的快速入门教程,涵盖了管道的基本使用、有缓冲和无缓冲管道的区别、管道的关闭、遍历、协程和管道的协同工作、单向通道的使用以及select多路复用的详细案例和解释。
117 4
Golang语言之管道channel快速入门篇
|
2月前
|
Go
Golang语言文件操作快速入门篇
这篇文章是关于Go语言文件操作快速入门的教程,涵盖了文件的读取、写入、复制操作以及使用标准库中的ioutil、bufio、os等包进行文件操作的详细案例。
67 4
Golang语言文件操作快速入门篇
|
2月前
|
Go
Golang语言之gRPC程序设计示例
这篇文章是关于Golang语言使用gRPC进行程序设计的详细教程,涵盖了RPC协议的介绍、gRPC环境的搭建、Protocol Buffers的使用、gRPC服务的编写和通信示例。
104 3
Golang语言之gRPC程序设计示例
|
2月前
|
安全 Go
Golang语言goroutine协程并发安全及锁机制
这篇文章是关于Go语言中多协程操作同一数据问题、互斥锁Mutex和读写互斥锁RWMutex的详细介绍及使用案例,涵盖了如何使用这些同步原语来解决并发访问共享资源时的数据安全问题。
88 4
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
68 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
1月前
|
算法 JavaScript 前端开发
垃圾回收算法的原理
【10月更文挑战第13天】垃圾回收算法的原理
24 0
|
2月前
|
Go 调度
Golang语言goroutine协程篇
这篇文章是关于Go语言goroutine协程的详细教程,涵盖了并发编程的常见术语、goroutine的创建和调度、使用sync.WaitGroup控制协程退出以及如何通过GOMAXPROCS设置程序并发时占用的CPU逻辑核心数。
53 4
Golang语言goroutine协程篇
|
2月前
|
Prometheus Cloud Native Go
Golang语言之Prometheus的日志模块使用案例
这篇文章是关于如何在Golang语言项目中使用Prometheus的日志模块的案例,包括源代码编写、编译和测试步骤。
56 3
Golang语言之Prometheus的日志模块使用案例
|
1月前
|
前端开发 中间件 Go
实践Golang语言N层应用架构
【10月更文挑战第2天】本文介绍了如何在Go语言中使用Gin框架实现N层体系结构,借鉴了J2EE平台的多层分布式应用程序模型。文章首先概述了N层体系结构的基本概念,接着详细列出了Go语言中对应的构件名称,包括前端框架(如Vue.js、React)、Gin的处理函数和中间件、依赖注入和配置管理、会话管理和ORM库(如gorm或ent)。最后,提供了具体的代码示例,展示了如何实现HTTP请求处理、会话管理和数据库操作。
33 0
下一篇
无影云桌面