Golang特辑---简单谈谈我所认为的垃圾回收机制

前言

最近工作有点不顺心,因为工作中发现很多基础知识不牢固。于是最近开始恶补。最近更博客频率会更加勤快一点,更加地偏向基础知识这一块,但不会记录的特别复杂,尽量大白话。如果记录不太准确,欢迎前来指正🙏


垃圾回收是什么?

每次了解一个技术点,第一个事情就是要弄明白,为什么需要它?

大家都学过c语言。我们要分配内存的时候都需要用到malloc这个东西。但是分配完之后总要去释放它呀,于是就用到free。怕就怕在,我们可能会人为的忘记去使用free释放资源。于是乎为程序分配的内存越来越多,最后会有内存泄露的风险。

在这里插入图片描述
不仅如此,在一些并发的情况,我们对内存的管理很难控制,因为你并不知道cpu何时会执行这个程序。程序运行时会出现各种各样的情况,那个时候如果内存这一块出现问题,我们排查起来会特别的困难。

大家发现没有,上述提到的问题,都是由于我们人工地去管理内存而导致的。那有没有可能让程序去帮我们手动的管理内存呢 ?

答案大家都知道了,现代大部分的编程语言都带有GC(Garbge Collection)垃圾回收 机制。在程序运行时,可以动态地帮助开发人员去管理内存。因此我们开发人员在编写代码的时候,也就不用考虑这一块,可以更多地沉入业务开发中。

但是这个东西也不是完全没有坏处,它会使你的程序占用更多的计算机资源。毕竟要实时监控你的程序状态,整个程序多占用点资源也无可厚非。为了提升程序正确性以及及时清理无用对象,这个取舍无可厚非。

同时还要搞清楚,垃圾回收的是哪一块内存?

程序中的内存分为堆内存(Heap)栈内存(Stack),后者一般是给函数局部变量和函数调用栈使用的,它们有个特点,生存周期特别短。所以也就没有回收的必要,用完直接清空即可,用白话讲就是死得快。

搞清除垃圾回收的是堆内存之后,我们接着往下看。


垃圾回收算法的分类

查了各种资料,垃圾回收算法目前大致分为三类。

引用计数法(reference counting):

这个实现比较简单,简单来说就是给每一个对象都赋予一个被引用计数,有其他东西引用这个对象时,给其计数加一,当引用这个对象的东西不引用对象时,这个计数减一。最后这个计数为零时则GC会将这个对象清除。说的有点绕,画个图帮助一下:

在这里插入图片描述
这个时候b对象引用a对象,所以a的被引用次数为1,而当b不引用a时,计数变为0,则GC会将a清除,体现在代码中可能是这样的:

b := &b{}
b.a = &a{} // b引用a,a的被引用次数为1.
b.a = nil // a的被引用次数为0,GC将其清除。

注意这里b对象的被引用情况先不做讨论。现代语言如python就是用这种垃圾回收算法的。它的优点简单易懂,回收无延迟,但是缺点也显而易见:

  • 需要加多一个字段来存储计数,增加了存储开销
  • 每次计数改变伴随加减法操作,增加了时间成本
  • 无法解决循环引用问题,需要手动介入,增加了程序的复杂性,后期不利于维护

标记清除法(mark and sweep)

这个是我们本篇文章的重点,后面的Golang的GC算法就是采用这一种方法。

标记清除法理解起来也不是很困难,就是分析对象是否可达。如果可达GC就标记一下这个对象,待全部对象标记完之后,未被标记的对象就会被清除。

在这里插入图片描述
上面这幅图中,a和b会先被标记,因为它们可以被GC找到,也就是可达。之后d对象又被a所引用,所以d也可达。而c和e找不到,于是没有被标记。待标记(Mark) 完之后,就会执行清除(Sweep) 了。把未被标记的对象清除掉。注意标记清除过程中,需要把程序先暂停,不然程序和GC并发执行,难保会有对象漏清除或多清除。这个暂停程序的过程也叫做Stop the world

Golang最开始就是采用上述的垃圾回收机制,好处是不会有对象会漏回收或者多回收。缺点也是很明显的,程序到某个时刻需要被Stop the world。如果对于实时性要求高的系统,这个是不被允许的。所以Go官方后面有对这个标记清除法进行了,具体Go的哪一代忘记了…

它们将标记清除法优化成了三色标记法,运用这个方法可以使得垃圾回收时,大部分阶段可以和运行中程序并发执行,不需要Stop the world。如果你是多核cpu的话,还可以并行。据官方自己的说法,可以提高30%-60%左右的速度。关于三色标记法,下文再介绍。


分代垃圾回收算法

这一个算法我了解的不是很多,但是效率上比上面两个要高。简单来说就是把程序中的对象分为两代 ------ 新生代和老年代。不同的代采用不同的垃圾回收算法。

在程序刚刚创建的对象称为新生代,已经存活了很久的对象叫老年代。比喻得特别形象,老年代对象有一个特点:该对象能够存活这么久,大概率是高频使用对象。对于这一类对象,GC可能减少回收频率甚至不回收它。而对于新生代对象,GC则采用普通的垃圾回收算法。

关于这两代得到垃圾回收算法有什么区别,这一块我也了解的不是很多。可能每个语言的实现都不同,所以抱歉这一块不能详细地记录。


三色标记法

讲完了垃圾回收的前世今生,但毕竟是Golang特辑嘛,还是要介绍一下go目前的垃圾回收算法—三色标记法

这个三色标记法理解起来不是很复杂,但是也不会记录得特别深入,因为关于源码类的阅读我也还没有能力做到。简单来说就是分为三种状态,白色、灰色以及黑色。最开始所有对象都是白色的,然后把其中全局变量和函数栈里的对象置为灰色。第二步把灰色的对象全部置为黑色,然后把原先灰色对象指向的变量都置为灰色,以此类推。等发现没有对象可以被置为灰色时,所有的白色变量就一定是需要被清理的垃圾了。

以下是三色标记法步骤:

  1. 标记前的准备操作(Mark Setup)
  2. 标记(Mark)
  3. 标记结束(Mark Termination)
  4. 清除(Sweeping)

接下来简单说下每一个步骤。

第一步Mark Setup,它和普通标记清除法的区别之一在于这。三色标记法有一个东西叫做写屏障(Write Barrier)。每当有一个对象写入时,都会使其上色成灰色。这样就避免了刚刚生成的对象就被清除的情况(多清除)。不过开启这个写屏障就需要Stop the world。怎么通知每一个goruntine停止呢?

以下摘选自互联网:

Go 语言采取的是合作式抢占模式(当前 1.13 及之前版本)。
这种模式的做法是在程序编译阶段注入额外的代码,更精确的说法是在每个函数的序言中增加一个合作式抢占点。
因为一个 goroutine 中通常有无数调用函数的操作,选择在函数序言中增加抢占点可以较好地平衡性能和实时性之间的利弊。
在通常情况下,一次 Mark Setup 操作会在 10-30 微秒之间。

有了这个函数序言,就可以通知到每一个goruntine停止了。这个合作市抢占模式有个缺点,官方仓库的一个issue指出了,各位有兴趣的话可以去看下。后续Go官方会考虑非合作抢占模式的优化。

第二步标记(Mark),这一步没什么好说的,就是给各个对象上色标记的过程,不过这个过程是不需要Stop the world,因为第一步里面我们已经把写屏障打开了,所以也不会有漏标和错标的情况出现。

第三步标记结束(Mark Termination),这一步就是把写屏障给关掉,同时还会计划下一次GC的工作计划,怎么才能更优化一下GC的效率。有关这一块后续会开一篇博客简单记录一下。特别注意,因为涉及到了写屏障,所以也是要Stop the world的。

最后一步清除(Sweeping),这一步没什么好说的,就是把未标记的对象清除,是可以和程序异步执行的。


总结

最后对三色标记法总结一下。

  1. 标记前准备,打开写屏障(需要stop the world
  2. 之后标记
  3. 标记结束,关闭写屏障(需要stop the world
  4. 清除

关于三色标记法那块部分摘选自互联网的各博客,再加以自己的语言,感谢您的观看🙏

  • 5
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值