关于网上各种GO语言GC文章的一些困惑和个人理解

1,549 阅读4分钟

目前网上有很多不错的介绍GO语言三色标记GC的文章和源码分析,这里推荐一篇个人感觉写的比较不错的从源码层面解析GO GC的博客Golang 垃圾回收剖析。看这些文章的过程中也产生了一些困惑,这里分享一下个人的思考,如果有不准确的地方欢迎大家批评指正。

困惑1:什么是root对象?

介绍go gc的文章都会提到,在三色标记的过程中,从root对象开始遍历找出所有的活跃对象,但我收集到的资料里没有提到什么是root。那什么是root对象呢?

  • 全局变量:可执行文件的.data和.bss域记录了全局变量的内存地址,被这写内存地址指向的内存是活跃对象。
  • go routine stack:go routine的的局部变量如果分配在堆上,需要GC来管理内存的回收,go routine的stack有类似于程序栈的栈结构,找到通过go routine stack里记录的变量的内存地址,可以找到活跃的局部变量。

通过广度优先遍历,可以从root开始找出所有的活跃对象,也包括被间接引用的对象(即被root直接引用的对象中包含的指针所引用的对象)。
p.s. 分配在程序栈上的对象,会随着函数调用结束栈指针变化而被操作系统回收,不需要GC来处理。

困惑2:怎么知道一个对象被引用还是没有被引用?

被引用
一个活跃的对象包括他所占用的内存和指向这块内存的指针,如果程序中存在一个上述的可被追踪的内存地址指向这块内存,则对象是活跃的。声明变量和给变量赋值的时候会改变引用关系。

一个对象怎么从被引用变成不被引用的

  1. 一个go routine执行结束了,这个go routine stack上记录的局部变量的内存地址所引用的对象就不可达了,不会被GC标记为活跃对象。
  2. 下面的例子中,给变量s重新赋值之后,s这个变量引用的是"bye",而"hello"这个字符串对象没有被指针引用,占用的内存就会被GC回收。而通过对象池,复用结构体,每次使用时重置结构体字段的值(此时结构体不会被回收),就能减少分配和回收内存的次数,提高程序的性能。
s := "hello"
s = "bye"

困惑3:GC线程跟用户线程并行执行标记的时候,有两次短暂的STW的原因

这是由于在GC进行标记的时候,用户进程还可以继续申请新的对象,如果这个新的对象被一个黑色对象引用,由于GC不会再去扫描黑色的对象,那么这个新申请的对象就不会被GC扫描到,而被错误的清除。
所以Go语言使用写屏障的机制(write barrier)来记录GC标记过程中对这些新申请对象的引用,并在GC标记的最后阶段重新扫描(re-scan)这部分变化的引用关系。
在GC开始的时候有一个短暂的STW来开启写屏障,可以理解成生成一个当前内存情况的"快照",后面可以根据这个"快照"判断哪些变化了的引用关系。在GC标记的最后阶段的STW就是来执行这个re-scan去处理write barrier记录下来的变化,此时如果不STW那么GC标记就会进入死循环永远无法完成。

困惑4:GC清除对象的时候,没有STW而且是跟用户线程并行执行的,如何避免对象引用关系变化而导致有对象被错误清除的?

这个困惑是在了解了Go的内存管理机制后有了答案的。因为Go语言会给对象记录一个GC generation。GC的并行标记和写屏障会保证在GC开始前已经存在的对象和标记过程中新分配的对象具有相同的gc geneartion。而标记结束以后,执行清除的过程中新产生的对象的GC generation不同,GC只会清除具有上次执行标记时的generation的对象。所以之后新产生的对象,在gc清除的时候无论是否被引用,都要等到下一个GC周期才有可能被清除。