摘要
golang 的三色标记法虽然没有 java 的内存回收机制成熟,但它细分了回收过程,通过写屏障技术,能和用户程序并发进行,这也一定程度的提高了内存回收速度。
一、为什么要有垃圾回收
我们都知道,当程序启动的时候,操作系统是会分配出栈区和堆区的,作为动态内存分配使用。
在栈区里分配的内存是可以自动管理的,一旦某个变量的作用域结束,就可以被自动回收了。
但是堆区就不是这样的了,堆区是属于程序员自己管理的区域,即使在某个作用域结束了,后续也能使用到该变量。
为此,程序员需要时刻关注内存的管理,否则将出现很多问题。例如内存一直在增长没有释放,则会出现内存溢出;内存释放后还继续访问,则会出现非法访问等。
因此,对内存的管理事关重要。然而,人为的管理内存始终存在隐患,谁也不能保证自己写的代码没有一点问题。
所以垃圾回收机制出现了,它是编程语言的设计者在程序运行时通过一定的策略,让闲置的内存被自动回收。
这也能让开发者更加专注于业务逻辑,减少额外的负担。
二、垃圾回收有哪些常用策略
1) 引用计数法
此算法为对象维护了一个计数值,当对象被引用时,计数值 +1,当对象的引用被释放时计数值 -1,直到计数值为 0 ,表示没有其他对象在使用它了,此时就可以进行回收动作了。
引用计数法算法简单,易于实现。但频繁的更新引用计数,也带来了一定的开销。而且对于循环引用的情况,计数值是归不了 0 的,此时就做不了回收了。
2) 标记-清除法
标记清除法是对对象定时的进行标记,分为正在被使用的和没有被使用这两类。
当标记完后就可以对没有被使用的这一类对象进行内存回收了。
标记清除法有个 Root 根对象,遍历搜索都是从 Root 根对象开始标记的。由于不存在对象跟 Root 循环引用的情况,所以总是能搜索到所有能达到的对象,依次标记。
如果循环对象没有被标记到,就表示没有被引用,就可以回收了,循环引用问题就解决了。
由于程序是动态在运行的,随时有可能会改变对象的引用指向。因此,在进行标记动作的时候需要 Stop the world(STW),也就是停止其他任务的执行,使得标记过程没有被打乱。
这也就意味着程序会短暂的停滞,对于响应要求高的程序而言,无疑是不能接受的。
三、golang 的垃圾回收
golang 采用了叫三色标记法的回收机制,它是第二种算法的变种,通过将标记清除过程细分了多个阶段,并采用了写屏障去感知引用的修改,使得垃圾回收动作能和用户程序并发的进行,大大缩短了 Stop The World 的同时,也保证了对象不被误清除。
三色标记法将对象分成了三种:
- 白色对象:未被使用的对象;
- 灰色对象:当前对象有引用对象,但是还没有对引用对象继续扫描过;
- 黑色对象,对上面提到的灰色对象的引用对象已经全部扫描过了,下次不用再扫描它的引用对象了。
当垃圾回收开始时,Go 会把根对象标记为灰色,其他对象标记为白色,然后从根对象遍历搜索,按照上面的定义去不断的对灰色对象进行扫描标记。
(这里的根对象可以理解为指向堆内存区块的指针)
当没有灰色对象时,表示标记完成,然后就可以开始清除白色对象了。
三色标记法在正式标记前会进行 Stop The World,以便启动写屏障。当启动好后,就会停止 Stop The World。然后开始标记对象,在这标记过程中,是可以和用户程序一起并发执行的。
当所有的对象都标记完,也就是没有灰色对象可遍历搜索时,会再一次的 STW,做第二次的扫描。
利用之前启动的写屏障,将标记期间有过引用修改的对象重新标记为灰色,保证对象不会被误清。
当第二次扫描结束时,就可以开始真正的清除动作了,而且也是可以跟用户程序一起并发执行的。
后面用户程序即使再 new 了对象,分配了内存,也不会进行标记动作了,相当于这些新的对象是下次 GC 要处理的了。
而对于原先被标记为白色的对象,也就是再也没有被使用的对象,程序是引用不到的了。此时就可以大胆并发清除,不需要再次 Stop the world 了。
触发时机
Go 允许手动触发垃圾回收,但一般开发者比较少介入内存的管理,更多是让运行时 runtime 根据下面 2 种情况来进行垃圾回收:
- 内存分配到一定大小时触发
- 一定时间内没有触发过垃圾回收,则会开始进行 GC,一般这个时间是 2 分钟
结尾
虽然有 GC 帮我们做内存的管理,但资源不是无限的,一旦内存上涨,那我们就得学会查找问题了。
当内存一直没有释放时,我们可以使用 pprof 这些性能分析工具,帮助我们去做内存分析。
另外,在写代码时我们也可以从下面几个点进行内存的优化,让我们的程序更加健壮。
- string 和 []byte 可以通过 unsafe 或 reflect 包进行强制转换,以减少内存拷贝。
- 如果频繁的临时对象需要创建,则可以使用 sync.Pool 来重用对象。减少垃圾回收。
- 如果能提前知道 slice 的大小,尽量预分配好它的容量,避免不断的 append 中,不断的扩容。