Golang垃圾回收

282 阅读12分钟

垃圾回收(Garbage Collection)

垃圾回收是一种自动内存管理机制,运行时自动回收程序中不再使用的内存空间。在手动管理内存的语言(如C/C++)中,程序员需要显式地分配和释放内存。而支持垃圾回收的语言(如Java、Go、Python等)则由垃圾回收器自动管理内存,从而避免程序员手动管理内存,减少了因忘记释放内存而导致的内存泄漏问题。

垃圾回收关键概念

  1. 引用(Reference):在垃圾回收的上下文中,引用是指一个对象指向另一个对象的指针。垃圾回收器通过跟踪这些引用来确定对象的可达性。引用可以是强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)或虚引用(Phantom Reference)。不同类型的引用影响垃圾回收器对对象生命周期的判断。

  2. 根集合(Root Set):根集合包括所有的全局变量、栈上的变量和寄存器中的对象引用。垃圾回收器从根集合开始,标记所有可达的对象。

  3. 可达性(Reachability):可达性分析是判断对象是否为垃圾的基础方法。它从一系列被称为根对象(如栈上引用、静态变量、JNI引用等)的地方出发,通过引用链遍历整个对象图,如果一个对象无法从根对象通过引用直接或间接到达,则判定该对象为不可达,即为垃圾。

  4. 停顿时间(Pause Times):垃圾回收过程中,程序可能会暂时停止,这被称为“Stop-The-World”事件。现代垃圾回收器尽量减少这种停顿时间,以提供更平滑的用户体验。

  5. 内存碎片(Memory Fragmentation):随着时间的推移,内存分配和回收可能会导致内存碎片,即内存中存在许多小的、不连续的空闲空间。这可能会影响内存分配的效率。

  6. 内存泄漏(Memory Leak):内存泄漏是指程序在申请内存后,无法释放不再使用的内存空间。这会导致程序占用的内存逐渐增加,可能导致程序运行缓慢或崩溃。垃圾回收器的目标之一就是检测并回收这些不再使用的内存,从而防止内存泄漏。

  7. 垃圾回收器(Garbage Collector):垃圾回收器是负责实现垃圾回收算法的系统组件。它自动管理内存,释放不再使用的对象所占用的内存空间,以便这些空间可以被重新利用。垃圾回收器的具体实现可能因编程语言、运行时环境或操作系统而异。

垃圾回收原理

垃圾回收的基本原理是确定程序中哪些内存区域(或对象)是“可达”的,即从根集合(如全局变量、栈上的引用等)出发,通过引用关系能够访问到的对象被认为是存活的;反之则是垃圾,即不再被程序中的任何活动部分所引用的内存。这些内存可以被回收,以便重新分配给新的对象。现代垃圾回收器的设计通常综合运用以下多种垃圾回收算法,并且支持并发和并行回收,以尽量减少垃圾回收带来的程序暂停时间(Stop-The-World),提高系统的总体性能。垃圾回收算法有:

  1. 引用计数(Reference Counting)

每个对象都有一个引用计数器,用于记录该对象被引用的次数。当对象被创建或者被引用时,引用计数加1;当引用失效时,引用计数减1。当某个对象的引用计数变为0时,说明该对象不再被使用,可以立即回收。引用计数算法的优点是不需要暂停程序进行垃圾回收,不会产生内存碎片。缺点是它不能处理循环引用的情况。

  1. 标记-清除(Mark-Sweep)

标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段遍历所有的对象,标记活动对象。清除阶段遍历堆,回收未被标记的对象所占用的空间。优点是能够有效回收不可达对象,缺点是回收过程中会产生大量不连续的内存碎片,影响内存分配的效率;需要暂停程序执行(STW)进行标记和清除操作。

  1. 标记-整理(Mark-Compact)

类似于标记-清除,但在清除阶段不是立即删除无用对象,而是将存活的对象往内存的一端移动,然后直接清理边界之外的内存。优点是解决了内存碎片问题;缺点是整理过程复杂,可能导致较长的STW时间。

  1. 复制(Copying)

将内存划分为两块相等大小的空间,每次只使用其中一块,当这一块满了或者需要进行垃圾回收时,将存活的对象复制到另一块,然后清除掉已使用的那一块的所有对象。然后交换两个内存块的角色,继续使用。优点是不会产生内存碎片;缺点是内存利用率低,且复制对象的开销较大。

  1. 分代收集(Generational Collection)

引用计数算法能够平滑的进行垃圾回收,而不会出现程序停止现象,经常出现于一些实时系统中,但它无法解决环形问题;而跟踪垃圾回收(标记-清除、标记-整理、复制),在每一次垃圾回收过程中,要遍历或者复制所有的存活对象,这是一个非常耗时的工作,一种好的解决方案就是对堆上的对象进行分区,对不同区域的对象使用不同的垃圾回收算法。分代收集就是根据对象生命周期的不同,将内存划分为多个区域(Young Generation和Old Generation),不同区域采用不同的算法。新生代中的对象通常存活时间较短,因此适合采用复制算法进行垃圾回收;老年代中的对象通常存活时间较长,因此适合采用标记-清除或标记-整理算法进行垃圾回收。

Golang垃圾回收器(Garbage Collector)

Go1.3采用了标记-清除算法,GC时需要较长的STW(Stop-The-World)停顿时间。Go1.5采用了并发三色标记算法和Go1.8引入了混合写屏障,使GC的大部分处理和应用程序的goroutine可以并发运行,减少了GC时的STW(Stop-The-World)停顿,从而提高程序的响应速度。

并发三色标记算法

并发三色标记算法将堆内存划分为三个区域:白色、灰色和黑色。白色表示未被标记的对象;灰色表示正在被标记的对象,但是它的引用还没有被完全扫描;黑色表示已被标记的对象,且它所有的引用都已经被扫描完毕。三色标记的基本过程如下:

  1. 所有对象开始时都是白色的。

  2. 从根集合出发,把第一次遍历到的对象标记为灰色,并从白色集合放入灰色集合。(启动STW扫描根集合)

  3. 遍历灰色集合,将灰色对象引用的白色对象标记为灰色,并从白色集合放入灰色集合;之后将此灰色对象标记为黑色,并从灰色集合放入黑色集合。

  4. 重复第3步,直到灰色集合为空。(并发标记完成后,需启动STW重新扫描栈上对象)

  5. 回收所有的白色标记的对象,也就是回收垃圾。

写屏障(Write Barrier)

并发三色标记算法,在不启动STW的情况下,应用程序在运行过程中会出现以下情况:

  1. 一个白色对象被黑色对象引用(白色被挂在黑色下)。

  2. 灰色对象与它之间的可达关系的白色对象遭到破坏(灰色同时丢了该白色)。

当上述1和2情况同时发生时,会产生将对象错误当成垃圾回收的现象。为了解决上面的问题,GC满足以下两种情况可以避免错误回收对象:

  1. 强三色不变式:不允许黑色对象引用白色对象。

  2. 弱三色不变式:黑色对象可以引用白色,白色对象存在其他灰色对象对它的引用,或者可达它的链路上存在灰色对象。

写屏障分为插入写屏障、删除写屏障和混合写屏障。插入写屏障实现的是强三色不变式;删除写屏障则实现了弱三色不变式;混合写屏障实现了变形的弱三色不变式。

插入写屏障(Insertion Write Barrier):对象被引用时触发的机制。当堆上白色对象被黑色对象引用时,白色对象被标记为灰色(栈上对象无插入写屏障)。为了保证栈的运行效率,插入写屏障只对堆上的内存对象启用,栈上的内存会在完成标记后, 启动STW重新扫描根集合。

删除写屏障(Deletion Write Barrier):对象被删除时触发的机制。如果灰色对象引用的白色对象被删除时,那么白色对象会被标记为灰色。 当被删除的对象为白色时,白色对象被标记为灰色。会存在一个对象即使被删除了最后一个指向它的指针也依旧可以活过这一轮,在下一轮GC中被清理掉。

混合写屏障(Hybrid Write Barrier):混合写屏障结合了插入和删除写屏障的优点,只需要在开始时并发扫描各个goroutine的栈,使其变黑并一直保持,这个过程不需要STW。而标记结束后,因为栈在扫描后始终是黑色的,避免了对栈启动STW重新扫描的过程,极大的减少了STW的时间。混合写屏障的规则如下:

  1. GC开始时将栈上可达对象全部标记为黑色(无需启动STW重新扫描栈上对象)。

  2. GC期间任何在栈上创建的新对象均为黑色。

  3. 被删除的对象标记为灰色。

  4. 被添加的对象标记为灰色。

GC触发条件

在Go语言中,垃圾回收(GC)触发的主要条件基于堆内存分配和使用情况。具体触发垃圾回收的条件主要有:

  1. 内存分配量达到阈值触发:当程序分配的堆内存总量达到了一个内部设定的阈值时,GC将会被触发。阈值由环境变量GOGC表示。

  2. 定期触发:如果一定时间内没有触发,就会触发新的GC,该触发条件由runtime.forcegcperiod变量控制,默认为2分钟。

  3. 手动触发:开发者可以通过runtime.GC()函数显式的触发GC。

Golang垃圾回收的基本流程

在不同的时期和版本中,GC的具体阶段划分略有差异,但大致可以分为以下几个核心阶段:

  1. 初始标记(Initial Marking):此阶段发生STW,垃圾回收器快速标记出根对象(全局变量、栈上的指针等可以直接或间接访问到的对象)及其引用的对象,并将这些对象标记为灰色。

  2. 并发标记(Concurrent Marking):在初始标记后,GC开始并发标记阶段,此时应用程序的可以继续执行。垃圾回收器并发地遍历灰色对象的引用链,将新的可达对象也标记为灰色,然后将灰色对象变更为黑色。这个过程中,写屏障被启用,以跟踪并处理在并发标记过程中新增或修改的引用。

  3. 标记终止(Mark Termination):在并发标记完成后,可能需要另一个STW阶段来完成标记的收尾工作,确保所有活跃对象都被正确地标记,并以及为接下来的清理阶段做准备。

  4. 并发清理(Concurrent Sweeping):回收所有在标记阶段被确定为不可达(即未被标记)的对象占用的内存。GC并发地遍历堆内存区域,回收未被标记的(即被认为是垃圾的)对象所占用的空间。

概括而言,Go语言GC的典型流程至少包含了标记和清理两个主要阶段,而且这两个阶段都有并发执行的部分,以及必要的STW阶段来保证数据一致性。随着Go语言版本的升级和GC算法的优化,具体阶段和细节会有所变动,但其目标始终是减少STW时间,提高整体性能。

GC性能优化

优化Go语言的垃圾回收性能通常涉及到这几个方面:减少堆内存分配;调整GC参数GOGC;监控和分析(使用内置的性能分析工具,如pprof和net/http/pprof包提供的接口,对程序进行CPU和内存使用分析,找出可能导致垃圾回收压力增大的根源)等。另外由于内存逃逸现象会产生一些隐式的内存分配,也有可能成为GC的负担。

FYI

A Guide to the Go Garbage Collector

Frequently Asked Questions (FAQ)

Golang垃圾回收