垃圾回收
所谓的垃圾就上不在需要的内存块,垃圾如果不清理,这些内存块就没有办法再次被分配使用。在不支持垃圾回收的编程语言中,这些垃圾内存就上泄露的内存。
1. 垃圾回收算法
常见的垃圾回收算法有3种
- 引用计数:对每个对象维护一个引用计数,当引用该对象的对象被销毁时,引用计数减1,当引用计数器为0时回收该对象
- 优点:对象可以很快被回收,不会出现内存耗尽或达到某个阈值时才回收
- 缺点:不能很好地处理循环引用,实施维护引用计数也有一定的代价
- 代表语言:Python,PHP,Swift
- 标记-清除:从根变量开始遍历所有引用的对象,引用的对象标记为"被引用",没有标记的对象被回收
- 优点:解决了引用计数的缺点
- 缺点:需要STW,即暂停程序运行
- 代表语言:Go(三色标记法)
- 分代收集:按照对象声明周期的长短划分不同的代空间,生命周期长的放入老年代,短的放入新生代,不同代有不同的回收算法和回收频率
- 优点:回收性能好
- 缺点:算法复杂
- 代表语言:Java
2.Go垃圾回收
2.1 垃圾回收的原理
垃圾回收的核心就是标记处那些内存还在使用,哪些内存不再使用了(即未被引用),把未被引用的内存回收,以供后续内存分配使用。
垃圾回收开始时从root对象扫描,把root对象引用的内存标记为“被引用”,考虑到内存块中存放的可能是指针,所以还需要递归地进行标记,全部标记完成后,只保留被标记的内存,未被标记的内存全部标记为未分配即完成了回收。
2.2 内存标记(Mark)
之前的博客有介绍了span数据结构,span中维护了一个个内存块,并有成员变量allocBits表示每个内存块的分配情况。在span的数据结构中还有另一个位图gcmarkBits(之前的文章中未被写出),用于标记内存块被引用的情况。
allocBits和gcmarkBits的数据结构完全一样,标记结束后就上内存回收,回收时将allocBits指向gcmarkBits,代表标记过的内存才是存活的内存,gcmarkBits则会在下次标记时重新分配内存。
2.3 三色标记法
三色对应了垃圾回收过程中对象的三种状态
- 灰色:对象还在标记队列中等待
- 黑色:对象已被标记,gcmarkBits对应的位为1(该对象不会再本次GC中被清理)
- 白色:对象未被标记,gcmarkBits对应的位为0(该对象会在本次GC中被清理)
- 初始化阶段:
- 所有对象最初都被标记为白色,表示尚未访问。
- 设置一个根集,根集通常包括当前的栈变量、全局变量以及Go运行时的数据结构等可以直接访问到的对象。这些根集中的对象被标记为灰色。
- 并发标记阶段:
- 从灰色对象开始,垃圾回收器会遍历这些对象引用的所有对象,并将其从白色改为灰色,表示这些对象已被发现但其引用的对象还未被检查。
- 同时,标记过程会递归进行,每当完成一个对象的引用检查(即将其引用的所有白色对象标记为灰色),该对象就会被标记为黑色,表示该对象及其所有引用链上的可达对象都已被访问过,不会再被重新访问。
- 这个过程是并发执行的,即垃圾回收器与程序的其他部分并行工作,以减少停顿时间。
- 重新扫描(重新标记)阶段:
- 由于标记阶段是并发进行的,程序可能在此期间修改了某些对象的引用关系,这可能导致一些本应标记为灰色或黑色的对象仍保持为白色。因此,需要有一个阶段来修正这种不一致,这个阶段称为重新扫描或重新标记阶段。
- 在这个阶段,垃圾回收器会暂停所有非垃圾回收相关的任务,再次检查灰色对象,并确保它们的引用关系已经被正确处理,新发现的白色对象会被标记为灰色继续处理,直至没有灰色对象剩余。
- 清理阶段:
- 当所有可达对象都被标记为黑色后,垃圾回收器知道所有白色对象都是不可达的,可以被安全地回收。
- 这个阶段会释放那些白色对象占用的内存空间,为新的分配做准备。
STW(Stop Whe World)就是停止所有的goroutine,专心做垃圾回收,待垃圾回收结束后再回复goroutine
STW时间的长短直接影响了应用的执行。
3. 垃圾回收优化
为了缩短STW的时间,Go也在不断地优化垃圾回收算法。
3.1 写屏障(Write Barrier)
- 写屏障就是让goroutine与GC同时运行的手段,写屏障可以打打缩短STW的时间。
- 写屏障类似一种开关,在GC的特定时机开启,开启后指针传递时会标记指针,即本轮不回收,下次GC时再确定。
- GC过程中新分配的内存会被立即标记,用的正是写屏障技术,即GC过程中分配的内存不会再本轮GC中回收
3.2 辅助GC(Mutator Assist)
为了防止内存分配过快,在GC执行过程中,如果goroutine需要分配内存,那么该goroutine会参与一部分GC的工作,即帮组GC做一部分工作,这个机制叫做Mutator Assist。
4. 垃圾回收的触发时机
4.1 内存分配量达到阈值触发GC
每次内存分配时都会检查当前内存分配量是否已达到阈值,如果达到阈值则立即启动GC。
阈值 = 上次GC内存分配量 * 内存增长率
内存增长率由环境变量GOGC控制,默认为100,即每当内存扩大一倍时启动GC
4.2 定期触发GC
默认情况下,最长2分钟触发一次GC。
通过变量forcegcperiod变量中被声明
4.3 手动触发
程序代码中也可以使用使用runtime.GC()来手动触发GC,主要用于GC的性能测试和统计。
5. GC性能优化
- GC性能与对象数量负相关,对象越多GC性能越差,对程序影响越大。
- 所以GC性能优化的思路之一就是减少对象分配的个数:比如使用对象复用或使用大对象组合多个小对象
- 内存逃逸现象会产生一些隐式的内存分配,也有可能成为GC的负担