从新手角度出发,了解GO垃圾回收的世界观和概念作为GO语言的新手,很难理解垃圾回收的世界观?怎么理解各种复杂的概念?放心
从新手角度出发,了解GO垃圾回收的世界观和概念
垃圾回收是什么?
GC,全称的Garbage Collection
,即垃圾回收,是一种自动内存管理机制
垃圾是怎么来的?
在GO语言日常的编程中,GC对程序员是无感的,大家自然感觉不到垃圾的产生和被回收发生在什么时候。
要说垃圾的产生就要先说栈和堆。在一个函数中,其内部的变量一般是存放在栈中的,这些变量占用的空间会随着函数执行结束被直接回收,这种情况是不需要GC的。
但是有一些其他情况,比如全局变量、闭包等,这些情况下就会存储在堆中,(也就叫做内存逃逸
),堆相对于栈是个更大的内存空间,由于不清楚某个函数会在什么时候调用全局变量或者闭包返回的值,所以这些数据就不会随着函数执行结束而销毁。但是,如果一直存在堆中,那么肯定会造成内存溢出
。这个时候就需要垃圾回收,将不使用的数据进行释放。
根对象是什么?
根对象在垃圾回收中又称为根集合
,他是垃圾回收的最先检查的对象。
我们都知道,在堆中,对象之间都是相互引用的,有的对象在引用其他对象时就是在使用中的,不能被清理,一旦对象没有引用其他对象或者没有被其他对象引用,那就是需要清理的垃圾。
根对象有哪些呢?比如全局变量,和goroutine的执行栈都是根对象
垃圾回收要做什么?
所谓垃圾回收,就是从根对象开始,类似树结构一样,一遍一遍查询堆中的没有被引用的对象,然后将他们清理掉的同时,不停的提升自己的效率,减少对程序运行的影响。
GO的垃圾回收
稍微有些了解的小伙伴都知道,GO的垃圾回收使用的是三色标记法和混合读写屏障。
那其实早期 的GO版本没有读写屏障,那为什么现在又加上了呢?在这之前,我们先来了解一下什么是三色标记法,当然我会用我的语言给大家介绍一下其原理,至于更深的源码了解,大家可以查阅其他资料。
三色标记法
之前我们介绍了根对象,他是GC扫描堆中的对象的起点,那如果我现在是一个GC,我要怎么遍历呢?
-
首先我会将所有的对象都放在
白色
的列表中,也就是表示所有对象都是需要清理
的 -
然后从
根对象出发
,也就说访问到根对象了,发现根对象是有引用其他对象或者被其他对象引用的,但是还没有扫描完根对象的引用关系,所以暂时将其从白色队列移到灰色
队列中,表示正在扫描中
-
从
灰色
队列的对象出发
(也就是刚扫描到的对象),在扫描其是否有引用的对象或者被其他对象引用,如果是,则将该对象移动到黑色
队列中,然后将和该对象有引用关系的对象
移动到灰色
队列中 -
重复第三步,直到灰色队列中没有对象为止,此时
白色
队列中任然没有被移走的对象就是不可达的对象
,也就是要回收的对象上面的步骤能用并发优化吗?可以自己思考一下,同时下面也会有解答。
当确定了需要回收的对象,下一步就要进行回收啦。但是我们知道,程序在运行中,堆中的对象引用是时刻变化的,也许下一个纳秒,原本白色的对象就有引用关系了,那这个时候就不能回收啦,那怎么解决这个问题呢?
聪明的你立刻就想到法子,我在垃圾回收的时候,阻止程序运行不就行啦。于是你就提出了STW
理论
STW理论
STW表示是Stop the world
,也就是暂停程序的一些运行,这个时候程序停止运行,堆中的对象引用关系不再发生变化,那你就可以快速的回收垃圾了。
那么创造GO语言的大神们也想到这么做,并且在已经在GO的早期版本实现啦。
但是作为新世纪的新语言,GO团队肯定希望减少STW的时间,毕竟世界静止并不是理想的状态,那么怎么优化GO的GC的逻辑呢?
并发标记清除法
GO的大神经过苦思冥想(我猜的)后,确定发挥GO语言的天生优势,那就是并发。
之前的介绍中我们留了一个关于并发的问题,在之前的介绍中,我们先扫描一遍然后做标记,然后在做清理,这种方式肯定是比较耗时的,如果我们可以在一遍清理的时候,确定了不可达的对象后,就立刻清理掉,是不是快了很多呢?
这就是并发标记清除法。
GO将GC分成了两部分,一个负责进行标记,我们称之为赋值器,一个负责清理,我们称之为回收器。拟人化理解就是一个人负责标记,一个人跟在后面按照标记进行清理。
并发标记的难点
只要是并发,就避免不掉一个问题,那就是同步性,如何保证标记和清除过程的正确性
就是一个很大的问题。
比如下面这种情况:
- 初始状态:假设某个黑色对象 C 指向某个灰色对象 A ,而 A 指向白色对象 B;
C.ref3 = C.ref2.ref1
:赋值器并发地将黑色对象 C 指向(ref3)了白色对象 B;A.ref1 = nil
:移除灰色对象 A 对白色对象 B 的引用(ref2);- 最终状态:在继续扫描的过程中,白色对象 B 永远不会被标记为黑色对象了(回收器不会重新扫描黑色对象),进而对象 B 被错误地回收。
这就是赋值器和回收器不同步导致的部分对象没法回收的情况,如果这种对象变多了,可能就会造成内存泄漏
的问题。
如果从三色标记法角度去理解,那就是两者不同步导致黑色对象原本只应该引用灰色对象,但是现在直接引用了白色对象造成的。
那怎么去解决这个问题呢?
写屏障
写屏障和强弱三色不变形是一个抽象的概念,理解起来有一定的难度,那我们就从实际的角度出发,去看看究竟啥是写屏障和强弱三色不变性
我们都知道,所有的赋值(也就是改变对象之间的引用
),都是赋值器干的事,所以我们只要对赋值器加一定的限制,不就可以解决这个问题了嘛
比如我们在赋值的时候,发现这样赋值会造成黑色对象直接引用白色对象
,那我们就阻止这次赋值,这样就避免了黑色直接引用白色的情况,而这种阻止的逻辑算法我们就称之为屏障
,由于是在写的时候发生的,我们就称之为写屏障
。
当然这样的逻辑算法GO已经实现了。
之前我们阻止黑色直接引用白色对象是因为这样会造成回收器无法扫描到这个白色对象,就不回进行回收。那么我们换个角度思考,只要能让这个白色对象被扫描到并正确回收(也就是被一个灰色的对象引用着
),那么也可以解决这个问题呢。
于是我们在上面的赋值器逻辑算法中加上这样一个情况
如果存在另一个正确的路径,将一个白色对象被灰色对象引用,并最终可以被扫描到,那么及时黑色对象直接引用白色对象也是可以的。
那么我们为了区分着两种逻辑,
将满足赋值器修改对象图,导致某一个黑色对象引用了白色对象的情况称之为弱三色不变性
将满足了弱三色不变性同时,也满足存在一条路径,将灰色对象引用白色对象的情况称之为强三色不变性
而以上就是我对写屏障和强弱三色不变性的理解。
这篇文章的目的就是为了从简要的角度出发,让你对GO的GC有个全局的理解,帮我们理解各种复杂的概念和逻辑。后面如果你想要了解更深的GO的GC,可以再查找其他的资料。
转载自:https://juejin.cn/post/7424034641378934823