【go基础】10.垃圾回收GC

目录

Go的GC设计原理

1. Go1.3之前——标记清除法

2. Go1.5——三色标记法+插入写屏障

3. Go1.8——三色标记法+混合写屏障

4. GC的触发时机

5. GC是否要触发的判断机制

6. 频繁GC对cpu的影响

7. runtime.SetFinalizer 指定 gc 时执行某些操作

8. SetMemoryLimit 设置gc触发的内存阈值

9. 设置某对象不能回收

10、开发时应注意

11、目前GC存在的问题

垃圾回收 trace过程


参考:

Go的GC设计原理


1. Go1.3之前——标记清除法

(1)步骤

  • 暂停程序,STW(Stop the World)
  • 找出所有存活对象并标记
  • 清除未标记的对象
  • 停止暂停,不断循环直到生命周期结束

(2)缺点

  • STW使程序出现卡顿
  • 标记需要扫描整个heap
  • 清除会产生很多heap碎片

2. Go1.5——三色标记法+插入写屏障

(1)三色标记法

  • 把所有对象标记为白色
  • 从根节点开始遍历所有对象,把遍历到的对象从⽩⾊集合放⼊灰⾊集合
  • 遍历灰⾊集合,将灰⾊对象引⽤的对象从⽩⾊集合放⼊灰⾊集合,之后将此灰⾊对象放⼊⿊⾊集合
  • 重复第三步, 直到灰⾊中⽆任何对象
  • 回收所有的⽩⾊标记表的对象

(2)无STW的问题

    会在标记过程中出现对象丢失的情况,导致引用对象被清除

    若以下两个条件同时满⾜,那么就会出现对象丢失的现象:

    条件1: ⼀个⽩⾊对象被⿊⾊对象引⽤ (⽩⾊被挂在⿊⾊下)

    条件2: 灰⾊对象与它之间的可达关系的⽩⾊对象遭到破坏 (灰⾊同时丢了该⽩⾊)

(3)强弱三色不变式

    强三色不变式:强制黑色对象不允许引用白色对象

    弱三色不变式:黑色对象可以引用白色对象,但要求白色的上游链路存在灰色

    三色标记若满足强/弱三色不变式之一,就可以保证对象不丢失

(4)屏障机制

    ① 插入写屏障

        对堆空间对象,在A对象引⽤B对象的时候,B对象被标记为灰⾊(满⾜强三⾊不变式)。

        对栈空间对象,不启用屏障。

        为了避免栈对象丢失,在扫描结束时,STW栈,将所有对象置为白色重新扫描。

        问题:STW重新扫描⼤约需要10~100ms

    ② 删除屏障

        对象被删除时,如果⾃身为灰⾊或者⽩⾊,标记为灰⾊。(满⾜弱三⾊不变式)

        问题:回收精度低,⼀个对象即使被删除了最后⼀个指向它的指针也依旧可以活过这⼀轮,在下⼀轮GC中被清理

3. Go1.8——三色标记法+混合写屏障

(1)混合写屏障

步骤:

  • 扫描栈上的对象,并全部标记为⿊⾊(之后不再进⾏第⼆次重复扫描,⽆需STW)
  • GC期间,任何在栈上创建的新对象,均标记为⿊⾊
  • 如果在堆上删除对象,标记为灰⾊
  • 如果在堆上添加对象,标记为灰⾊
特点:
  • 栈空间不启用屏障,只在堆空间启用屏障。
  • 满⾜变形的弱三⾊不变式,结合了插⼊、删除写屏障两者的优点。
  • 不需要STW,效率高。
(2)gc过程
  • 初始化阶段
  • 标记阶段
  • 标记终止阶段
  • 清理阶段

有2次STW,但时间都很短

在第二阶段标记阶段,事件可能较长,会使用多个线程(Marking Assist)执行加快效率。

4. GC的触发时机

以下三个方式都会调用 gcStart 执行 GC

(1)用户手动调用 runtime.GC

runtime.GC时会获取当前的 GC 循环次数,然后设值为 gcTriggerCycle 模式调用 gcStart 进行循环。

这个调用对调用方是阻塞的

在垃圾收集期间也可能会通过 STW 暂停整个程序。

仅推荐测试使用。

(2)监控线程 runtime.sysmon 定时触发 gc

go main在启动时会后台运行一个线程定时执行 runtime.sysmon 函数,主要用来检查死锁、运行计时器、调度抢占、GC 。

它会执行 runtime.gcTrigger 中的 test 函数来判断是否应该进行 GC。

由于 GC 可能需要执行时间比较长,所以运行时会在应用程序启动时在后台开启一个用于强制触发垃圾收集的 Goroutine 执行 forcegchelper 函数。不过 forcegchelper 函数一般情况下会被 goparkunlock 函数一直挂起,直到 sysmon 触发GC 校验通过,才会将该被挂起的 Goroutine 放转身到全局调度队列中等待被调度执行 GC。

(3)申请内存 runtime.mallocgc

对象在进行内存分配的时候会按大小分成微对象、小对象和大对象三类分别执行 tiny malloc、small alloc、large alloc。

Go 的内存分配采用了池化技术,类似 CPU 这样的设计,分为了三级缓存:每个线程单独的缓存池mcache、中心缓存 mcentral 、堆页 mheap

tiny malloc、small alloc 都会先去 mcache 中找空闲内存块进行内存分配,如果 mcache 中分配不到内存,就要到 mcentral 或 mheap 中去申请内存,这个时候就会尝试触发 GC;而对于 large alloc 一定会尝试触发 GC 因为它直接在堆页上分配内存。

5. GC是否要触发的判断机制

runtime.gcTrigger 中的 test 函数最终会根据三个策略,判断是否应该执行GC:
  • gcTriggerHeap:按堆大小触发,堆大小和上次 GC 时相比达到一定阈值则触发(与环境变量 GOGC有关,默认值100
  • gcTriggerTime:按时间触发,如果超过 forcegcperiod(默认2分钟) 时间没有被 GC,那么会执行GC
  • gcTriggerCycle:没有开启垃圾收集,则触发新的循环

6. 频繁GC对cpu的影响

gc的主要目的是管理内存,确保不再使用的对象被正确回收,从而释放内存空间。然而,gc过程本身也需要消耗CPU资源。

可以说gc是对cpu和内存资源的一种权衡

为了平衡这两者,不同的垃圾回收算法和策略被设计出来。例如,某些算法会尝试在低CPU负载时进行回收,以减少对应用程序性能的影响。而另一些算法则可能更注重内存管理,即使这意味着更高的CPU消耗。

7. runtime.SetFinalizer 指定 gc 时执行某些操作

比如可以通过它来设置一个钩子,每次 GC 完之后检查一下内存情况,动态设置 GOGC 值,由此来控制 GC 的频率。 聊聊两个Go即将过时的GC优化策略 - luozhiyun`s Blog

func finalizerHandler(f *finalizerRef) {
    // 为 GOGC 动态设值
    getCurrentPercentAndChangeGOGC()
    // 重新设置回去,否则会被真的清理
    runtime.SetFinalizer(f, finalizerHandler)
}
runtime.SetFinalizer(&i, finalizerHandler)

为了达到最大化利用内存,减少 GC 次数的目的,那么我们可以将 GOGC 设置为:

(可使用内存最大百分比 - 当前占内存百分比)/当前占内存百分比 * 100

也就是说如果有一台机器,全部内存都给我们应用使用,应用当前占用 10%,也就是 100M,那么:

GOGC = (100%-10%)/10% * 100 = 900

8. SetMemoryLimit 设置gc触发的内存阈值

go1.19新特性
// 设置内存上限600M,当内存达到阈值时会自动触发gc
debug.SetMemoryLimit(600 * 1024 * 1024)

通过内置的方式实现了 GC 的控制,控制 GC 内存触发阈值达到减少 GC 的目的

9. 设置某对象不能回收

runtime.KeepAlive

10、开发时应注意

  • GC时会有短暂的STW,暂停所有goroutine,在高并发和实时性要求高的场景会存在问题,避免过多GC
  • 尽量复用对象,使用对象池(sync.Pool),避免频繁内存分配和释放
  • 避免长时间持有大量对象
  • 监控GC性能:pprof,适当优化
  • 设置环境变量修改GC阈值,go.19 SetMemoryLimit动调修改

11、目前GC存在的问题

  • 短暂的STW会暂停所有gouroutine
  • GC时cpu和内存压力大
  • 导致内存碎片化,太分散,加大内存分配失败几率

垃圾回收 trace过程


1、关闭gc
$ go build
$ GOGC=off ./project > /dev/null

2、开启gctrace,发送请求

$ GODEBUG=gctrace=1 ./project > /dev/null
$ hey -m POST -c 100 -n 10000 "http://localhost:5000/search?term=topic&cnn=on&bbc=on&nyt=on"

代码逻辑中接收到请求就去申请一块内存

3、pprof监控

开启pprof代码:

import _ "net/http/pprof”

go func() {
    http.ListenAndServe("localhost:5000", http.DefaultServeMux)
}()

// 进入pprof
go tool pprof http://localhost:5000/debug/pprof/allocs

//查看进程内存占用
(pprof) top 6 -cum     

//查看具体占内存的代码
(pprof) list {函数名}   

  • 29
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值