gc垃圾回收算法原理

1.三色标记法

以下是Golang GC算法的里程碑:

  • v1.1 STW
  • v1.3 Mark STW, Sweep 并行
  • v1.5 三色标记法
  • v1.8 三色标记法+混合写屏障

go的gc是基于 标记-清扫算法,并做了一定改进,减少了STW的时间。

1.标记-清扫(Mark And Sweep)算法

此算法主要有两个主要的步骤:

  • 标记(Mark phase)
  • 清除(Sweep phase)

第一步,找出不可达的对象,然后做上标记。

第二步,回收标记好的对象。

操作非常简单,但是有一点需要额外注意:mark and sweep算法在执行的时候,需要程序暂停!即 stop the world

2.标记-清扫(Mark And Sweep)算法存在什么问题?

标记-清扫(Mark And Sweep)算法这种算法虽然非常的简单,但是还存在一些问题:

  • STW,stop the world;让程序暂停,程序出现卡顿。
  • 标记需要扫描整个heap
  • 清除数据会产生heap碎片

这里面最重要的问题就是:mark-and-sweep 算法会暂停整个程序。

3.三色并发标记法

1.创建3个集合,白色,灰色,黑色
2.开始STW暂停协程
3.将全部对象全部放入白色集合(标记白色)
4.遍历root对象,把遍历到的对象,从白色集合放入灰色集合(标记灰色),灰色都是root的根对象
5.遍历灰色集合,把灰色对象引用的对象也放入灰色集合(标记灰色),自己放入黑色集合(标记黑色)。无论怎么样,遍历灰色对象之后,自己就变成黑色,有引用的话,引用的对象就变成灰色。
6.重复5步骤,直到没有灰色为止
7.清除白色对象


root对象(根对象)包括:全局变量+指向堆内存的栈指针
全局变量
执行栈:每个协程都包涵自己的执行栈,这些执行栈指向指向堆上的内存
寄存器:可能是指针

STW(写屏障):目的:防止GC扫描时内存变化引起混乱,写屏障让协程与GC同时运行的手段
通俗的讲:就是在gc跑的过程中,可以监控对象的内存修改,并对对象进行重新标记。(实际上也是超短暂的stw,然后对对象进行标记),在gc过程中产生的新对象,一律变为灰色

4.gc和用户逻辑如何并行操作?

标记-清除(mark and sweep)算法的STW(stop the world)操作,就是runtime把所有的线程全部冻结掉,所有的线程全部冻结意味着用户逻辑是暂停的。这样所有的对象都不会被修改了,这时候去扫描是绝对安全的。

Go如何减短这个过程呢?标记-清除(mark and sweep)算法包含两部分逻辑:标记和清除。

我们知道Golang三色标记法中最后只剩下的黑白两种对象,黑色对象是程序恢复后接着使用的对象,如果不碰触黑色对象,只清除白色的对象,肯定不会影响程序逻辑。所以:清除操作和用户逻辑可以并发。

5.进程新生成对象的时候,GC该如何操作呢?不会乱吗?

Golang为了解决这个问题,引入了 写屏障这个机制。

写屏障:该屏障之前的写操作和之后的写操作相比,先被系统其它组件感知。

通俗的讲:就是在gc跑的过程中,可以监控对象的内存修改,并对对象进行重新标记。(实际上也是超短暂的stw,然后对对象进行标记)

在上述情况中,新生成的对象,一律都标位灰色!

6.那么,灰色或者黑色对象的引用改为白色对象的时候,Golang是该如何操作的?

看如下图,一个黑色对象引用了曾经标记的白色对象。

这时候,写屏障机制被触发,向GC发送信号,GC重新扫描对象并标位灰色。

img

因此,gc一旦开始,无论是创建对象还是对象的引用改变,都会先变为灰色。

2.堆栈

1.内存分配中的堆和栈

栈(操作系统):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

堆(操作系统): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。

2.堆栈缓存方式

栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放。

堆则是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。

申请到 栈内存 好处:函数返回直接释放,不会引起垃圾回收,对性能没有影响。

3.Golang 的GC触发时机是什么?

峰值:默认内存扩大一倍,启动gc。(当前堆内存占用是上次gc后的两倍)
定期:默认2min触发一次gc,src/runtime/proc.go:forcegcperiod
手动:runtime.gc()


设置GC:
// 将 GC 触发阈值设置为 50%
runtime.SetGCPercent(50)

4.Golang的内存模型,为什么小对象多了会造成gc压力

Golang 的内存模型是基于逃逸分析的。当一个对象在编译时无法确定其存活范围时,就会被认为是逃逸对象,会被分配在堆上。相反,如果一个对象的生命周期可以确定在函数内部,就会被分配在栈上。

对于小对象来说,其分配和回收的开销相对较小。但当有大量小对象时,就会出现一些问题:
1.大量小对象占用的内存空间会比较大,导致 GC 扫描的范围增加,GC 压力增大。
2.小对象的分配和回收比较频繁,也会增加 GC 的负担。
3.大量小对象的内存碎片化会影响内存的利用率。

所以在设计 Golang 程序时,要尽量避免产生大量小对象,比如通过对象池、Sync.Pool 等技术来复用对象。同时也要关注内存使用情况,适当增加内存限制,避免 GC 压力过大。

5.查看GC相关的性能报告,线程数,协程数等资源


//可以使用 pprof 工具收集 GC 相关的性能数据,并生成报告进行分析,例如:
//访问:http://localhost:6060/debug/pprof
import _ "net/http/pprof"
func main() {
    // 启动 pprof 服务
    go func() {
        http.ListenAndServe("localhost:6060", nil)
    }()

    // 你的程序逻辑
}

posted @ 2022-02-28 15:02  Jeff的技术栈  阅读(235)  评论(0编辑  收藏  举报
回顶部