垃圾即为不再需要的内存块。如果这些垃圾不清理,无法再次被分配使用。在不支持垃圾回收(GC)的编程语言中,这些垃圾内存就会泄露。Golang的垃圾回收也是内存管理的一部分。了解垃圾回收,最好先了解内存分配原理。
业界常见的垃圾回收算法有以下几种:
维护每个对象的引用计数,当引用该对象的对象被销毁时,引用计数减1。当引用计数为0时,回收该对象。
从根变量开始遍历所有引用的对象,引用的对象标记为“被引用”,没有被标记的进行回收。
按照对象生命周期长短划分不同的代空间,生命周期长的放入老年代,短的放入新生代,不同代有不同的回收算法和回收频率。
简单来说,垃圾回收的核心是标记出哪些内存还在使用中(即被引用到),哪些内存不再使用(即未被引用),并回收未被引用的内存,以供后续内存分配时使用。
下图展示了一段内存,内存中既有已分配的内存,也有未分配的内存。垃圾回收的目标是找到那些已经分配但没有对象引用的内存,并回收掉。
上图中,内存块1、2、4号位上的内存块已被分配(数字1代表已被分配,0代表未分配)。变量a, b为指针,指向内存的1、2号位。内存块的4号位曾经被使用过,但现在没有任何对象引用,需要被回收。
垃圾回收开始时从root对象开始扫描,把root对象引用的内存标记为“被引用”。由于内存块中存放的可能是指针,所以还需要递归地进行标记,全部标记完成后,只保留被标记的内存,未被标记的全部标识为未分配,完成回收。
在内存分配时,span数据结构中维护了一个个内存块,并由位图allocBits表示每个内存块的分配情况。在span数据结构中还有另一个位图gcmarkBits用于标记内存块的引用情况。
如上图所示,allocBits记录了每块内存分配情况,gcmarkBits记录了每块内存标记情况。标记阶段对每块内存进行标记,有对象引用的内存标记为1(图中灰色所示),没有引用的保持默认为0。
三色标记法对应了垃圾回收过程中对象的三种状态:
例如,当前内存中有A~F共6个对象,根对象a, b分别引用了对象A、B,而B对象又引用了对象D。GC开始前各对象状态如下图所示:
接着开始扫描根对象a、b:
由于根对象引用了对象A、B,那么A、B变为灰色对象。接下来分析灰色对象,分析A时,A没有引用其他对象,很快转入黑色;B引用了D,则B转入黑色的同时还需要将D转为灰色,进行接下来的分析。
最终,所有黑色对象被保留下来,白色对象被回收。
Golang中的STW(Stop The World)是指停掉所有的goroutine,专心进行垃圾回收,待回收结束后再恢复goroutine。STW时间的长短直接影响应用执行,时间过长对一些web应用来说是不可接受的,这也是广受诟病的原因之一。为了缩短STW时间,Golang不断优化垃圾回收算法,很大程度上改善了这个问题。
写屏障让goroutine与GC同时运行,虽然不能完全消除STW,但可以大大减少STW时间。在GC特定时机开启写屏障,指针传递时会把指针标记,即本轮不回收,下次GC时再确定。
GC过程中,如果goroutine需要分配内存,那么这个goroutine会参与部分GC工作,帮助GC做一部分工作,这个机制叫作Mutator Assist。
每次内存分配时都会检查当前内存分配量是否已达到阀值,如果达到则立即启动GC。阀值公式如下:
阀值 = 上次GC内存分配量 * 内存增长率
内存增长率由环境变量GOGC控制,默认为100,即每当内存扩大一倍时启动GC。默认情况下,最长2分钟触发一次GC,这个间隔在 src/runtime/proc.go:forcegcperiod
变量中被声明。
程序代码中也可以使用 runtime.GC()
来手动触发GC,这主要用于GC性能测试和统计。
GC性能与对象数量负相关,对象越多GC性能越差,对程序影响越大。因此,GC性能优化的思路之一是减少对象分配个数,比如对象复用或使用大对象组合多个小对象等。此外,由于内存逃逸现象,也可能产生隐式的内存分配,增加GC负担。
通过了解Golang的垃圾回收机制,可以更好地优化代码并提高程序性能。
如果您喜欢我的文章,请点击下面按钮随意打赏,您的支持是我最大的动力。
最新评论