豆瓣9.1:终于有人把Go垃圾回收讲明白了

博文小编

2023-05-30

豆瓣鲜有的原创9.1高分Go语言图书《Go专家编程》,最近出版社的编辑老师告知刚刚出了第二版,这是一本定位于 Go 语言进阶的书籍,主要讲解 Go 语言特性的实现机制,但为了照顾新手程序员,也为了循序渐进、由浅入深地展开介绍,在介绍特性前也会从基础用法讲起,

在讲解 Go 语言实现原理时,书中尽可能地使用源码中的数据结构,并配以适量的图文来帮助理解。除了对 Go 语言特性的介绍,还包含一些精心设计的测试题目,用于帮助读者检验自己的能力水平。此外,还收录了一些发生在真实项目中的陷阱案例,这些案例大都源自商业项目或开源项目,值得参考。

本文从书中节选一段关于垃圾回收的内容。希望对大家学习理解有所帮助。

所谓垃圾就是不再需要的内存块,这些垃圾如果不清理就没办法再次被分配使用,在不支持垃圾回收的编程语言里,这些垃圾内存就是泄漏的内存。Go 的垃圾回收(GC)也是内存管理的一部分,了解垃圾回收最好先了解前面介绍的内存分配的原理。

01 垃圾回收算法

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

◎ 引用计数:对每个对象维护一个引用计数,当引用该对象的对象被销毁时,引用计数减 1,当引用计数器为 0 时回收该对象。

优点:对象可以很快地被回收,不会出现内存耗尽或达到某个阈值时才回收。
缺点:不能很好地处理循环引用,而且实时维护引用计数也有一定的代价。
代表语言:Python、PHP、Swift。
◎ 标记—清除:从根变量开始遍历所有引用的对象,引用的对象标记为“被引用”,没有标记的对象被回收。

优点:解决了引用计数的缺点。

缺点:需要 STW,即暂时停止程序运行。

代表语言:Go(其采用三色标记法)。

◎ 分代收集:按照对象生命周期的长短划分不同的代空间,生命周期长的放入老年代,而短的放入新生代,不同代有不同的回收算法和回收频率。

优点:回收性能好。

缺点:算法复杂。

代表语言:Java。

02 Go垃圾回收

1)垃圾回收的原理

简单地说,垃圾回收的核心就是标记出哪些内存还在使用中(即被引用到),哪些内存不再使用了(即未被引用),把未被引用的内存回收,以供后续内存分配时使用。

下图展示了一段内存,内存中既有已分配的内存,也有未分配的内存,垃圾回收的目标就是把那些已经分配但没有对象引用的内存找出来并回收。

上图中,内存的 1、2、4 号位上的内存块已被分配(数字1 表示已被分配,0表示未分配)。变量 a、b 为指针,指向内存的 1、2 号位。内存块的4号位曾经被使用过,但现在没有任何对象引用了,就需要被回收。

垃圾回收开始时从 root 对象扫描,把 root对象引用的内存标记为“被引用”,考虑到内存块中存放的可能是指针,所以还需要递归地进行标记,全部标记完成后,只保留被标记的内存,未被标记的内存全部标记为未分配即完成了回收。

2)内存标记(Mark)

前面介绍内存分配时,介绍过 span 的数据结构,span 中维护了一个个内存块,并由一个位图 allocBits 表示每个内存块的分配情况。在span的数据结构中还有另一个位图gcmarkBits,用于标记内存块被引用的情况。

如下图所示,allocBits 记录了每块内存的分配情况,而gcmarkBits记录了每块内存的标记情况。标记阶段对每块内存进行标记,有对象引用的内存标记为1(如图中灰色部分所示),没有引用到的内存保持为 0(默认)。

allocBits 和 gcmarkBits 的数据结构是完全一样的,标记结束就是内存回收,回收时将allocBits 指向 gcmarkBits,代表标记过的内存才是存活的,gcmarkBits 则会在下次标记时重新分配内存,设计非常巧妙。

3)三色标记法

前面介绍了对象标记状态的存储方式,还需要有一个标记队列来存放待标记的对象,可以简单想象成把对象从标记队列中取出,将对象的引用状态标记在 span 的 gcmarkBits 中,把对象引用到的其他对象再放入队列。

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

◎ 灰色:对象还在标记队列中等待。

◎ 黑色:对象已被标记,gcmarkBits 对应的位为1(该对象不会在本次GC中被清理)。

◎ 白色:对象未被标记,gcmarkBits 对应的位为0(该对象会在本次GC中被清理)。

例如,当前内存中有 A~F 共 6 个对象,根对象a、b本身为栈上分配的局部变量,根对象 a、b 分别引用了对象 A、B,而 B 对象又引用了对象D,则 GC 开始前各对象的状态如下图所示。

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

接着开始扫描根对象 a、b,如下图所示。

由于根对象引用了对象 A、B,那么 A、B变为灰色对象。接下来开始分析灰色对象,分析 A 时,A 没有引用其他对象,很快就转为黑色对象,B引用了 D,则 B 转入黑色的同时还需要将D转为灰色对象进行接下来的分析,如下图所示。

上图中的灰色对象只有 D,由于 D 没有引用其他对象,所以 D 转为黑色对象,标记过程结束,如下图所示。

最终,黑色对象会被保留下来,白色对象会被回收。

4)Stop The World

印度电影《苏丹》中描述摔跤的一句台词是:“所谓摔跤,就是把对手控制住,然后摔倒他”

对于垃圾回收来说,在回收过程中也需要控制内存的变化,否则在回收过程中指针传递会引起内存引用关系变化,如果错误地回收了还在使用的内存,那么结果将是灾难性的。

Go 中的 STW(Stop The World)就是停止所有的goroutine,专心做垃圾回收,待垃圾回收结束后再恢复 goroutine。

STW 时间的长短直接影响了应用的执行,时间过长对于一些Web应用来说是不可接受的,这也是其广受诟病的原因之一。

03 垃圾回收优化

为了缩短 STW 的时间,Go 也在不断地优化垃圾回收算法。

1)写屏障(Write Barrier)

前面说过 STW 的目的是防止 GC 扫描时内存变化而停止 goroutine,而写屏障就是让goroutine 与 GC 同时运行的手段。虽然写屏障不能完全消除 STW,但是可以大大缩短 STW 的时间。

写屏障类似一种开关,在 GC 的特定时机开启,开启后指针传递时会标记指针,即本轮不回收,下次 GC 时再确定。GC 过程中新分配的内存会被立即标记,用的正是写屏障技术,即 GC 过程中分配的内存不会在本轮 GC 中回收。

2)辅助 GC(Mutator Assist)

为了防止内存分配过快,在 GC 执行过程中,如果 goroutine 需要分配内存,那么该goroutine会参与一部分 GC 的工作,即帮助 GC做一部分工作这个机制叫作Mutator Assist。

04 垃圾回收的触发时机

1)内存分配量达到阈值触发 GC

每次内存分配时都会检查当前内存分配量是否已达到阈值,如果达到阈值则立即启动GC。

阈值 = 上次 GC 内存分配量 × 内存增长率

内存增长率由环境变量 GOGC 控制,默认为100,即每当内存扩大一倍时启动GC。

2)定期触发 GC

默认情况下,最长 2 分钟触发一次 GC,这个间隔在src/runtime/proc.go:forcegcperiod变量中被声明:

3)手动触发

程序代码中也可以使用 runtime.GC()来手动触发GC,主要用于GC 的性能测试和统计。

05 GC性能优化

GC 性能与对象数量负相关,对象越多 GC 性能越差,对程序影响越大。

所以 GC 性能优化的思路之一就是减少对象分配的个数,比如对象复用或使用大对象组合多个小对象,等等。

另外,由于内存逃逸现象会产生一些隐式的内存分配,也有可能成为 GC 的负担。

读者评论

相关专题

相关博文

  • (三)spring cloud云服务架构代码结构详细讲解

    Omaye 2017-11-28

    上一篇我们介绍了spring cloud云服务架构 - particle云架构代码结构,简单的按照几个大的部分去构建代码模块,让我们来回顾一下: 第一部分: 针对于普通服务的基础框架封装(entity、dao、service、co...

    Omaye 2017-11-28
    1283 1 4 4
  • Spring Cloud构建微服务架构—配置中心

    醜人 2017-11-17

    Spring Cloud Config是Spring Cloud团队创建的一个全新项目,用来为分布式系统中的基础设施和微服务应用提供集中化的外部配置支持,它分为服务端与客户端两个部分。其中服务端也称为分布式配置中心,它是一个独立的微服务...

    醜人 2017-11-17
    526 2 2 2
  •  Spring Cloud构建微服务架构—服务容错保护(Hystrix服务降级)

    Spring Cloud构建微服务架构—服务容错保护(Hystrix服务降级)

    醜人 2017-11-17

    在开始使用Spring Cloud Hystrix实现断路器之前,我们先拿之前实现的一些内容作为基础,其中包括: eureka-server工程:服务注册中心,端口:1001 eureka-client工程:服务提供者,两个实例启动...

    醜人 2017-11-17
    502 2 2 2