简介
在JVM中,GC采用可达性分析法来判断对象是否死亡;在python虚拟机中,GC采用引用计数法加循环检测器来判断对象是否死亡,而在golang中,使用的是三色表记法来判断对象是否死亡。
什么是三色抽象
总所周知在GC时,通常会出现 stop the world问题(即垃圾回收器抢占CPU资源导致程序暂停)。在golang中为了解决这个问题,我们引出了三色抽象法,至于如何解决接下来我们详细说说。
首先我们需要了解三色指的是哪三色,分别有什么含义:
- 白色对象:潜在的垃圾,其内存可能会被垃圾收集器回收。
- 黑色对象:活跃对象,包括不存在任何引用外部指针的对象,以及根对象可达的对象。
- 灰色对象:活跃对象,因为存在指向白色对象的外部指针,所以垃圾收集器回扫描这些对象的子对象
看到这里大家可能会有一些疑惑:
- 黑色对象中的不存在任何引用外部指针的对象,以及根对象可达的对象,是什么意思?
- 为什么存在两种活跃对象?
问题1其实可以画图解释,由于三色标记法没有指明根对象,所以是和可达性分析法结合使用的。
途中三个个黑色对象就对应了不存在任何引用外部指针的对象,以及根对象可达的对象
而关于问题2,我们在接下来将会解答。
回收过程
首先我们需要了解有哪些对象在三色抽象中被视作根对象:
-
全局变量:这些是程序的全局状态,包括在函数外部声明的所有变量。
-
当前活动的 goroutine 栈:每个 goroutine 都有自己的栈,栈上的变量都被视为根对象。
-
其他运行时数据结构:这些包括一些内部的运行时数据结构,如 finalizer 队列。
在垃圾收集器刚刚开始工作时,所有的对象都为白色,不存在黑色对象。这时根对象被标记为灰色,垃圾收集器只会从灰色对象集合中取出对象开始扫描,当灰色对象集为空时,标记阶段就结束了。
屏障技术
在标记过程中,我们会遇到一个新的问题,由于多数现代处理器会乱序执行指令以达到性能最大化,导致的乱序问题。也可以理解为在并发场景下用户线程与gc线程同时进行,导致引用关系被改变。所以为了在并发、增量标记算法中保证标记正确性,不会出现类似于悬挂指针类似问题,我们就需要满足三色不变性:
- 强三色不变性:
- 黑色对象不会指向白色对象(在golang中体现为如果一个黑色对象引用一个新对象,该对象会被立即标记为灰色或黑色),只会指向灰色对象或者黑色对象。
- 弱三色不变性:
- 黑色对象指向白色对象必须包含一条灰色对象经由多个白色对象的可达路径。
只要满足上述两种三色不变性之一就符合要求。
屏障技术主流使用的有两种:插入写屏障和删除写屏障 (为什么没有读屏障呢?这时由于读屏障需要在用户程序使用时插入代码,对用户程序性能影响大)
-
插入写屏障(Insertion Write Barrier):在引用关系被修改之前插入一些额外的操作。例如,当一个对象 A 要引用另一个对象 B 时,插入写屏障会首先将 B 标记为可达,然后再修改 A 的引用。这样,即使在修改引用的过程中发生了垃圾回收,B 也不会被错误地回收。
-
删除写屏障(Deletion Write Barrier):在引用关系被修改之后插入一些额外的操作。例如,当一个对象 A 不再引用另一个对象 B 时,删除写屏障会在修改 A 的引用之后,检查 B 是否还被其他对象引用。如果 B 不再被任何对象引用,那么它就会被标记为垃圾。
增量和并发
屏障技术都是为了服务增量和并发场景下的gc,我们如此如此麻烦就是为了解决gc的STW问题。增量垃圾收集器会增量的标记和清除垃圾,降低应用程序暂停的最长时间。并发垃圾收集器利用多核的计算机资源在用户程序执行时并发标记与清除垃圾。
GC发生条件
STW的垃圾收集器能够有效控制堆大小。Golang默认配置会在堆内存达到上次垃圾收集的两倍时出发新的一轮垃圾收集。即默认值为100。
而并行垃圾收集其由于需要与程序一起运行,所以无法精确控制内存大小,在达到目标前就触发gc。
注意
在Golang和Java中都存在主动gc的指令,但是它们却有很大差异。在Java中存在 System.gc()命令,它会建议JVM开始进行gc,但是最终是否gc是取决于JVM的。但是在Gloang中,只要使用了runtime.GC指令,该方法就会在调用时阻塞调用方,直到当前垃圾收集循环完成。
垃圾回收过程(引子)
回收过程则是调用一系列函数对内存进行释放,这就涉及到Golang内存分配部分知识了。会遍历稀疏内存的runtime.mspan来检查每个内存页对象是否为白色对象,如果是就释放该部分内存。具体回收过程可能会在后续发布。