我们在程序中定义一个变量,会在内存中开辟相应内存空间进行存储,当不需要此变量后,需要手动销毁此对象,并释放内存。而这种对不再使用的内存资源进行自动回收的功能即为垃圾回收(Garbage Collection,缩写为GC),是一种自动内存管理机制
引用计数通过在对象上增加自己被引用的次数,被其他对象引用时加1,引用自己的对象被回收时减1,引用数为0的对象即为可以被回收的对象,这种算法在内存比较紧张和实时性比较高的系统中使用比较广泛,如php,Python等。
优点:
缺点:
追踪式算法(可达性分析)的核心思想是判断一个对象是否可达,如果这个对象一旦不可达就可以立刻被GC回收了,那么我们怎么判断一个对象是否可达呢?
第一步从根节点开始找出所有的全局变量和当前函数栈里的变量,标记为可达。第二步,从已经标记的数据开始,进一步标记它们可访问的变量,以此类推,专业术语叫传递闭包。当追踪结束时,没有被打上标记的对象就被判定是不可触达。
优点:
和引用计数法相比,有以下缺点:
标记清除算法是最常见的垃圾收集算法,标记清除收集器是跟踪式垃圾收集器,其执行过程可以分成标记(Mark)和清除(Sweep)两个阶段:
优点:
缺点:
它把内存空间划分为两个相等的区域,每次只使用其中一个区域。在垃圾收集时,遍历当前使用的区域,把存活对象复制到另一个区域中,最后将当前使用的区域的可回收对象进行回收。
实现:
优点:
GC-root
对象出发,将可达的对象复制到另外一块内存后直接清理当前这块内存即可。缺点:
在标记可回收的对象后将所有存活的对象压缩到内存的一端,使他们紧凑地排列在一起,然后对边界以外的内存进行回收,回收后,已用和未用的内存都各自一边。
优点:
缺点:
为了解决原始标记清除算法带来的长时间STW, Go从v1.5版本实现了基于三色标记清除的并发垃圾收集器,在不暂停程序的情况下即可完成对象的可达性分析,三色标记算法将程序中的对象分成白色、黑色和灰色三类:
三色标记法属于增量式GC算法,回收器首先将所有对象标记成白色,然后从gc root出发,逐步把所有可达的对象变成灰色再到黑色,最终所有的白色对象都是不可达对象。
具体实现:
gc root
对象出发,扫描所有可达对象标记为灰色,放入待处理队列优点:
缺点:
三色标记法存在并发性问题,
想要在并发或者增量的标记算法中保证正确性,我们需要达成一下两种三色不变性中的任意一种。
垃圾收集中的屏障技术更像是一个钩子方法,它是在用户程序读取对象、创建新对象以及更新对象指针时执行的一段代码,根据操作类型的不同,我们可以将它们分成读屏障和写屏障两种,因为读屏障需要在读操作中加入代码片段,对用户程序的性能影响很大,所以变成语言往往都会采用写屏障保证三色不变性。
当一个对象引用另外一个对象时,将另外一个对象标记为灰色,以此满足强三色不变性,不会存在黑色对象引用白色对象。
在灰色对象删除对白色对象的引用时,将白色对象置为灰色,其实就是快照保存旧的引用关系,这叫STAB(snapshot-at-the-beginning),以此满足弱三色不变性。
v1.8版本之前,运行时会使用插入写屏障保证强三色不变性;
在v1.8中,组合插入写屏障和删除写屏障构成了混合写屏障,保证弱三色不变性;该写屏障会将覆盖的对象标记成灰色(删除写屏障)并在当前栈没有扫描时将新对象也标记成灰色(插入写屏障):
写屏障会将被覆盖的指针和新指针都标记成灰色,而所有新建的对象都会被直接标记成黑色。
Go语言的垃圾收集可以分成清除终止、标记、标记终止和清除四个不同阶段:
清理终止阶段
标记阶段
_GCmark
、开启写屏障、用户程序协助(Mutator Assists
)并将根对象入队;Goroutine
的栈、全局对象以及不在堆中的运行时数据结构,扫描Goroutine
栈期间会暂停当前处理器;标记终止阶段
_GCmarktermination
并关闭辅助标记的用户程序;清理阶段
_GCoff
开始清理阶段、初始化清理状态并关闭写屏障;Goroutine
申请新的内存管理单元时就会触发清理;当满足触发垃圾收集的基本条件:允许垃圾收集、程序没有崩溃并且没有处于垃圾循环;
注:运行时会通过如下所示的runtime.gcTrigger.test
方法决定是否需要触发垃圾收集,该方法会根据三种不同方式触发进行不同的检查。
func (t gcTrigger) test() bool {
if !memstats.enablegc || panicking != 0 || gcphase != _GCoff {
return false
}
switch t.kind {
case gcTriggerHeap:
return memstats.heap_live >= memstats.gc_trigger
case gcTriggerTime:
if gcpercent < 0 {
return false
}
lastgc := int64(atomic.Load64(&memstats.last_gc_nanotime))
return lastgc != 0 && t.now-lastgc > forcegcperiod
case gcTriggerCycle:
return int32(t.n-work.cycles) > 0
}
return true
}
GC
结束时存活对象的内存达到某个比例时就触发GC
。(默认配置会在堆内存达到上一次垃圾收集的2倍时,触发新一轮的垃圾收集,可以通过环境变量GOGC
调整,在默认情况下他的值为100,即增长100%的堆内存才会触发GC
);比如一次回收完毕后,内存的使用量为5M,那么下次回收的机制则是内存分配达到10M的时候,也就是说,并不是内存分配越多,垃圾回收频率越高。sysmon
检测出一段时间内(由runtime.forcegcperiod
变量控制,默认为2分钟)没有触发过GC
,就会触发新的GC。runtime.GC()
强制触发GC
减少堆内存的分配是最好的优化方式。比如合理重复利用对象;避免string
和byte[]
之间的转化等,两者发生转换的时候,底层数据结构会进行复制,因此导致gc效率会变低,少量使用+
连接string
,Go里面string
是最基础的类型,是一个只读类型,针对他的每一个操作都会创建一个新的string
,如果是少量小文本拼接,用“+”
就好,如果是大量小文本拼接,用strings.Join
;如果是大量大文本拼接,用bytes.Buffer
。
优化努力的方向:
转自:
https://juejin.cn/post/7111515970669117447
- EOF -
Go 开发大全
参与维护一个非常全面的Go开源技术资源库。日常分享 Go, 云原生、k8s、Docker和微服务方面的技术文章和行业动态。
关注后获取
回复 Go 获取6万star的Go资源库
分享、点赞和在看
支持我们分享更多好文章,谢谢!