我们知道GO语言垃圾回收采用三色标记法。我们来深入理解这个全流程
目录
标记终止阶段最重要的任务是:计算下一次GC时需要达到的堆目标。
标记准备阶段
标记准备阶段最重要的任务是清扫上一阶段GC遗留需要请扫的垃圾。因为我们采用了懒清理算法,所以在执行下一次GC时,可能还有对象没有被清理。同时,标记准备阶段还会重置各种状态和指标,启动专门标记的协程,统计需要扫描的任务数,开启写屏障,启动标记协程等等。我们来细看这个过程。
计算标记协程的数量
GO语言规定后台标记协程消耗CPU 25%。为了实现这个,最简单的方式时以当前逻辑处理器P来计算,开启协程的数量是0.25P。但是若P=1这种情况怎么办?他要求在标记周期T内,每个P都需要花25%的时间来执行后台标记协程。
切换到后台标记协程
在标记阶段开启了STW,当关闭STW再次开启所有协程时,每个P都会进入新一轮的调度循环,调度循环开始时,调度器会判断是否在GC阶段,若是则会判断当前P是否执行后台标记协程。
并发标记阶段
这个阶段最为复杂。
后台标记任务有3种模式和4种flag。
后台标记flag有4种。用于指定后台标记协程的不同行为。
根对象扫描
所谓的根对象指的是全局变量(在.bss和.data段内存中),span中finalizer,和所有协程栈。
全局变量
全局扫描是通过编译器和运行时共同努力获取的。只有在运行时才能确定全局变量被分配到虚拟内存哪个位置,(若有指针,则指针指向的位置在运行时会变化),编译器确定全局变量中哪些位置包含指针。
指针是如何找到对应对象的位置的呢?首先确定heapArea,进而找到page对应的mspan,最终找到mspan中的第几个元素。
找到这个元素后,位图gcmarkBit位图对应的元素bit设置为1,表明已被标记,同时将该元素放入标记队列。
finalizer
在标记期间,后台标记协程后遍历mspan中specials链表,扫描finalizer元素对象。注意,我们不能将finalizer所在的span放入根对象,否则我们将失去回收这个对象的机会,同时需要扫描析构器对象fn,因为fn可能指向堆内存并可能回收。
GO 语言文件描述符使用finalizer,这样文件描述符不再被使用,即时用户忘记关闭文件描述符也可以被回收。其次,CGO中C函数内存分配不受GO语言GC回收管理,这时候我们也可以使用finalizer回收内存。
栈扫描
每个栈帧函数的参数和局部变量都需要扫描,确认该对象是否在使用,若在使用,则需要扫描对应的位图看看这个对象是否包含指针。
什么情况下对象可能没被使用呢?
例如这个例子,t对象则没被使用。
栈对象。栈对象指在栈上可以被寻址的对象,例如上述的t,可以在栈上&获取地址的对象,所以其在栈上一定有地址。编译器会在编译时将所有栈对象记录下来保存在二叉搜素树中。接着会遍历栈对象看是否被引用,若没被引用则可以回收。
扫描灰色对象
在进行根对象扫描时,将标记的对象放入本地队列中,如果本地队列放不下,则放入全局队列。这是为了最大粒度避免使用锁。
同刚才上述的, 确认该对象是否在使用,若在使用,则需要扫描对应的位图看看这个对象是否包含指针。为了快速获得这个对象是否包含指针mheap中包含一个重要字段bitmap:
bitmap用位图形式记录了每个指针大小的内存信息,每个指针都会2个bit分别表示当前内存是否应该继续扫描和是否包含指针。
若有指针,则取出指针值对其扫描,根据指针找到span中的对象,如果引用是白色对象则标记为灰色。
标记终止阶段
标记终止阶段主要完成统计用时,统计强制开始GC次数,更新下一次出发GC需要达到的堆目标,关闭写屏障,唤醒后台请扫协程等等。标记终止阶段会再次STW。
标记终止阶段最重要的任务是:计算下一次GC时需要达到的堆目标。
也就是当堆内存到达多少时,需要出发GC。这里使用调步算法。
辅助标记
当我们并发标记阶段,扫描内存的同时,用户协程也不断分配内存。那么这样标记将永无止境。解决这个问题使用的是辅助标记。
辅助标记必须标记阶段进行,由于用户协程分配超过额度的内存则不得不暂停并切换为辅助标记工作。具体地,当GC并发标记阶段,当用户分配内存时,会先检查是否完成指定扫描工作。协程中会指定当前协程可以被分配的内存大小,当用户协程分配内存时,会先从本地资产池获取,若本地不足则从全局资产池获取。用户协程一开始是没有资产的,所有资产来自后台标记协程。若无法从本地资产池获取也无法从全局资产池获取则需要执行辅助标记协程。
辅助标记工作完成后,如果本地还没有足够的资源。则可能有两个原因。1,当前协程被强占。2当前逻辑处理器P没有多余的标记工作可以做,陷入休眠,后台标记协程扫描了足够任务后将其唤醒。
屏障技术
辅助技术解决GC正常结束的问题。屏障技术解决的是准确性的问题。我们使用的是混合屏障技术。即使用插入屏障和删除屏障。
插入屏障
删除屏障。删除屏障可能把一个垃圾对象标记为灰色,这个不会影响准确性,因为浮动垃圾会在下一次垃圾回收中被回收。
垃圾清扫![](https://i-blog.csdnimg.cn/blog_migrate/921c977fb2cd8ff9dbef7449213a5cb7.png)
程序中只有一个垃圾清扫协程,并且与用户协程同时进行。
清扫采用懒清扫策略。
即执行少量清扫后,让渡自己的执行权,不要一直执行,因此触发下一阶段垃圾回收后,可能有没有被清理的内存将等他们清理完。
清扫以span为单位。这里有个巧妙的机制,实际上就是我这次清理完的对象就是我下次分配的对象,这样就能重用内存。
若gcmarkBits全为0则整个span将被mheap回收,并跟新基数树。
可以看出这种回收并没有将内存最终释放到操作系统。而是以再次组织内存的方式以便下次使用。
辅助清扫
刚才是辅助标记,现在是辅助清扫。触发辅助清扫两个原因:1,向mcentral申请内存时,2大对象分配时。这两个时间会判断已清扫的page是否大于目标page,若不成立则执行辅助清扫。
系统驻留内存清除
后台清扫协程是在程序开始时启动,并且启动一个,保证只占CPU1%的执行时间。其基本思路是:在基数树中找到连续的没有被操作系统回收的内存。
总结
GO语言引入辅助标记,辅助清扫,系统驻留内存存储,混合写屏障等策略保证能快速执行垃圾回收而且不影响用户协程,默认下,内存达到上一次GC标记内存的2倍后触发回收。并发阶段目标是消耗25%的CPU后台标记协程,清扫阶段,GO语言采用懒清扫策略。并留1%CPU执行系统驻留内存清除。