图解Golang的GC垃圾回收算法

虽然Golang的GC自打一开始,就被人所诟病,但是经过这么多年的发展,Golang的GC已经改善了非常多,变得非常优秀了。

以下是Golang GC算法的里程碑:

  • v1.1 STW
  • v1.3 Mark STW, Sweep 并行
  • v1.5 三色标记法
  • v1.8 hybrid write barrier

经典的GC算法有三种: 引用计数(reference counting)标记-清扫(mark & sweep)复制收集(Copy and Collection)

Golang的GC算法主要是基于 标记-清扫(mark and sweep) 算法,并在此基础上做了改进。因此,在此主要介绍一下 标记-清扫(mark and sweep)算法 ,关于 引用计数(reference counting)复制收集(copy and collection) 可自行百度。

标记-清扫(Mark And Sweep)算法

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

  • 标记(Mark phase)
  • 清除(Sweep phase)

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

第二步,回收标记好的对象。

操作非常简单,但是有一点需要额外注意: mark and sweep 算法在执行的时候,需要程序暂停!即 stop the world

也就是说,这段时间程序会卡在哪儿。故中文翻译成 卡顿

我们来看一下图解:

开始标记,程序暂停。程序和对象的此时关系是这样的:

然后开始标记,process找出它所有可达的对象,并做上标记。如下图所示:

标记完了之后,然后开始清除未标记的对象:

然后 垃圾 清除了,变成了下图这样。

最后,停止暂停,让程序继续跑。然后循环重复这个过程,直到 process 生命周期结束。

标记-清扫(Mark And Sweep)算法存在什么问题?

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

  • STW,stop the world;让程序暂停,程序出现卡顿。
  • 标记需要扫描整个heap
  • 清除数据会产生heap碎片

这里面最重要的问题就是:mark-and-sweep 算法会暂停整个整个程序。

Go是如何面对并这个问题的呢?

三色并发标记法

我们先来看看Golang的三色标记法的大体流程。

首先:程序创建的对象都标记为白色。

gc开始:扫描所有可到达的对象,标记为灰色

从灰色对象中找到其引用对象标记为灰色,把灰色对象本身标记为黑色

监视对象中的内存修改,并持续上一步的操作,直到灰色标记的对象不存在

此时,gc回收白色对象。

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

好了,大体的流程就是这样的,让我们回到刚才的问题:Go是如何解决 标记-清除(mark and sweep) 算法中的卡顿(stw,stop the world)问题的呢?

gc和用户逻辑如何并行操作?

标记-清除(mark and sweep)算法的STW(stop the world)操作,就是runtime把所有的线程全部冻结掉,所有的线程全部冻结意味着用户逻辑是暂停的。这样所有的对象都不会被修改了,这时候去扫描是绝对安全的。

Go如何减短这个过程呢?标记-清除(mark and sweep)算法包含两部分逻辑:标记和清除。

我们知道Golang三色标记法中最后只剩下的黑白两种对象,黑色对象是程序恢复后接着使用的对象,如果不碰触黑色对象,只清除白色的对象,肯定不会影响程序逻辑。所以: 清除操作和用户逻辑可以并发。

标记操作和用户逻辑也是并发的,用户逻辑会时常生成对象或者改变对象的引用,那么标记和用户逻辑如何并发呢?

process新生成对象的时候,GC该如何操作呢?不会乱吗?

我们看如下图,在此状态下:process程序又新生成了一个对象,我们设想会变成这样:

但是这样显然是不对的,因为按照三色标记法的步骤,这样新生成的对象A最后会被清除掉,这样会影响程序逻辑。

Golang为了解决这个问题,引入了 写屏障 这个机制。

写屏障:该屏障之前的写操作和之后的写操作相比,先被系统其它组件感知。

通俗的讲:就是在gc跑的过程中,可以监控对象的内存修改,并对对象进行重新标记。(实际上也是超短暂的stw,然后对对象进行标记)

在上述情况中, 新生成的对象,一律都标位灰色!
即下图:

那么,灰色或者黑色对象的引用改为白色对象的时候,Golang是该如何操作的?

看如下图,一个黑色对象引用了曾经标记的白色对象。

这时候,写屏障机制被触发,向GC发送信号,GC重新扫描对象并标位灰色。

因此,gc一旦开始,无论是创建对象还是对象的引用改变,都会先变为灰色。

参考文献:

Golang's Real-time GC in Theory and Practice

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • Golang捕获panic堆栈信息的讲解

    golang当中panic的时候如果启动的goroutine比较多,刷的信息满屏都是,在终端工具上因为刷的信息太多,找不到前边的信息,因此很有必要程序自己捕获panic,并且将错误信息输出到文件当中,以便定位排查问题. Golang捕获panic堆栈信息 func PanicTrace(kb int) []byte { s := []byte("/src/runtime/panic.go") e := []byte("\ngoroutine ") line := [

  • golang时间、时区、格式的使用方法

    前几天,因为需要实现海外服务端定时停机,涉及到时区的概念.网上搜索了一下,大部分都是谈time.Format中的Layout,非常不成体系,这里就简单总结一下其中的时间初始化.时区转化及格式转换. 开发中,我们对时间的使用是比较多的,其应用场景,按照使用概率,从大到小,通常是: 获取当前或数据库中存储的时间 比较两个时间点的先后 显示打印时间 时区转换 对应到go,也就是几个基本定义: 时间点与时间段:Time,Duration.好比MVC中的M. 时 区:Location,在时间转换上,好比是

  • Golang 使用接口实现泛型的方法示例

    在C/C++中我们可以使用泛型的方法使代码得以重复使用,最常见例如stl functions:vector<int> vint or vector<float> vfloat等.这篇文章将使用interface{...}接口使Golang实现泛型. interface{...}是实现泛型的基础.如一个数组元素类型是interface{...}的话,那么实现了该接口的实体都可以被放置入数组中.注意其中并不一定必须是空接口(简单类型我们可以通过把他转化为自定义类型后实现接口).为什么i

  • Golang 日期/时间包的使用详解

    golang 的日期时间包:time 的使用方式. time package 包含了 time.Time 时间对象 及 构建此时间对象的一些方法(time.Unix(), time.Parse()) golang 可精确到 nanosecond,故相应的函数返回值或参数都已纳秒为单位,我们可以借助time.ParseDuration(durationString string)友好的生成纳秒度量的时间跨度值 golang 的时间格式串Layout固定为 2006-01-02 15:04:05 g

  • golang中range在slice和map遍历中的注意事项

    golang中range在slice和map遍历中的注意事项 package main import ( "fmt" ) func main() { slice := []int{0, 1, 2, 3} myMap := make(map[int]*int) for _,v :=range slice{ if v==1 { v=100 } } for k,v :=range slice{ fmt.Println("k:",k,"v:",v) }

  • Golang 函数执行时间统计装饰器的一个实现详解

    背景 最近在搭一个新项目的架子,在生产环境中,为了能实时的监控程序的运行状态,少不了逻辑执行时间长度的统计.时间统计这个功能实现的期望有下面几点: 实现细节要剥离:时间统计实现的细节不期望在显式的写在主逻辑中.因为主逻辑中的其他逻辑和时间统计的抽象层次不在同一个层级 用于时间统计的代码可复用 统计出来的时间结果是可被处理的. 对并发编程友好 实现思路 统计细节的剥离 最朴素的时间统计的实现,可能是下面这个样子: func f() { startTime := time.Now() logicSt

  • 简单谈谈Golang中的字符串与字节数组

    前言 字符串是 Go 语言中最常用的基础数据类型之一,虽然字符串往往都被看做是一个整体,但是实际上字符串是一片连续的内存空间,我们也可以将它理解成一个由字符组成的数组,Go 语言中另外一个与字符串关系非常密切的类型就是字节(Byte)了,相信各位读者也都非常了解,这里也就不展开介绍. 我们在这一节中就会详细介绍这两种基本类型的实现原理以及它们的转换关系,但是这里还是会将介绍的重点主要放在字符串上,因为这是我们接触最多的一种基本类型并且后者就是一个简单的 uint8 类型,所以会给予 string

  • golang 并发安全Map以及分段锁的实现方法

    涉及概念 并发安全Map 分段锁 sync.Map CAS ( Compare And Swap ) 双检查 分断锁 type SimpleCache struct { mu sync.RWMutex items map[interface{}]*simpleItem } 在日常开发中, 上述这种数据结构肯定不少见,因为golang的原生map是非并发安全的,所以为了保证map的并发安全,最简单的方式就是给map加锁. 之前使用过两个本地内存缓存的开源库, gcache, cache2go,其中

  • golang读取文件的常用方法总结

    使用go语言读取文件的各种方式整理. 一次性加载到内存中 // * 整个文件读到内存,适用于文件较小的情况 //每次读取固定字节 //问题容易出现乱码,因为中文和中文符号不占一个字符 func readAllIntoMemory(filename string) (content []byte, err error) { fp, err := os.Open(filename) // 获取文件指针 if err != nil { return nil, err } defer fp.Close(

  • golang time包的用法详解

    在我们编程过程中,经常会用到与时间相关的各种务需求,下面来介绍 golang 中有关时间的一些基本用法,我们从 time 的几种 type 来开始介绍. 时间可分为时间点与时间段,golang 也不例外,提供了以下两种基础类型 - 时间点(Time) - 时间段(Duration) 除此之外 golang 也提供了以下类型,做一些特定的业务 - 时区(Location) - Ticker - Timer(定时器) 我们将按以上顺序来介绍 time 包的使用. 时间点(Time) 我们使用的所有与

随机推荐