golang slice 内存泄露_获得了“官方自己都会踩的”坑认证:slice 类型内存泄露的逻辑...

本文探讨了Go语言中slice内存泄露的原因,指出持有子切片引用可能导致大切片无法回收,进而引发内存泄露。文章提到,尽管Go的垃圾收集器在大多数情况下表现良好,但在高性能场景下,减少堆上对象分配仍然对优化服务延迟至关重要。同时,文章建议在使用slice复用时注意其大小,避免因用户输入导致的内存浪费。
摘要由CSDN通过智能技术生成

点击上方蓝色“Go语言中文网”关注我们,领全套Go资料,每天学习 Go 语言

本文作者:曹春晖 Xargin

原文链接:https://xargin.com/logic-of-slice-memory-leak/

Go 101 总结了几个可能导致内存泄露的场景:

https://gfw.go101.org/article/memory-leaking.html

goroutine 阻塞在 channel,time.Ticker 不使用但未 stop,以及 for 循环里用 defer 导致泄露,这三个场景其实已经比较常见了,这里就不说了。

我们来看看子切片截取为什么会导致内存泄露。

因为 Go 是一门带 GC 的语言,虽然官方宣传 GC stw 在 1ms 以内,但服务的延迟并不仅仅取决于 stw,用户的 goroutine 如果触发了堆上分配内存的操作,也很有可能进入到协助标记流程(newobject->mallocgc->gcAssist),这个“协助标记”也会导致相应的用户 goroutine 延迟大幅度上升。我们总会听到有人吹 stw 1ms 以内没有影响,这其实是一个相当不负责的结论。目前 Go 的实现只是把 GC 的成本进行了均摊,例如标记成本有一部分被均摊给了用户的 goroutine。

GC 优化在高并发的场景下是一定有必要的。通过减少堆上创建的对象来降低标记的压力,一方面可以节省 GC 整体使用的 CPU,最终也就大幅缩减了用户服务的延迟,那些说不用优化的笑笑就好。

在优化 GC 时,最直接的思路就是对创建对象进行复用,官方提供了 sync.Pool 来帮助用户对他们的应用对象进行复用。这里看 Go 的基本类型:map 和 slice。

sync.Pool 在复用对象时,需要我们在 Get 或 Put 时对对象进行清空操作,这个清空是由用户完成的,对于一个 slice 来说,清空操作很简单:sl := sl[:0]。但对于一个 map 来说,就没这么简单了。虽然官方对清空 map 也进行了一些优化:这里[1] 和 这里[2]。不过显然 map 依然不是一个适合复用的结构。

因为 slice 相比 map 要容易复用,在性能敏感的场景,只要能用 slice 来代替 map 就都换成了 slice + sync.Pool 来进行复用,fasthttp 里有很多这方面的实践,之前也有人做了比较好的总结,参考 这里[3] 和 这里[4]

嗯,既然知道了 slice 很方便复用,大家都喜欢它,来看看为什么 slice 的复用可能会造成内存泄露。

下面这段程序是这位[5] 朋友提供的 demo 的改版:

package main

import (
"fmt"
"runtime"
"time"
)

type P struct {
Age int
}

func getPartOfSlice() []*P {
var s = make([]*P, 0, 10000)
for i := 0; i < 10000; i++ {
var p = &P{i}
runtime.SetFinalizer(p, func(x *P) { println("gc happen on p", x) })
s = append(s, p)
}
return s[100:101]
}

func main() {
var k = getPartOfSlice()

// type 1
// print then gc
//fmt.Println(k[0])
//runtime.GC()

// type 2
// gc then print
runtime.GC()
fmt.Println(k[0])
time.Sleep(time.Hour)
}

type 1 表示在 runtime.GC 的时候,已经没有代码持有 k 的引用了,而 type 2 则表示在发生 GC 时,k 依然被持有。

显而易见,只要你持有子切片的某个对象,大切片被截掉的那些元素就是没有办法进行回收的。大概是下面这样:

3bcf3df5ce486b30d7ddef4224050e1e.png

subslice

在引用 slice 时要特别小心,因为有些 slice 的大小是动态生成的(比如可能依赖外部参数),所以也可能 99.99% 的 slice 大小在 <10,但只要有几个大 slice 就导致你的应用程序占用内存大幅增加,如果 slice 的大小依赖于用户输入,甚至会导致发生偶发的 OOM。

这个坑踩的人很多,比如:

writev 的 leak[6]

获得了“官方自己都会踩的 Go 语言”坑认证。

如果你的 slice 大小是用户输入决定的,在往 pool 里放的时候,应该提前判断一下 slice 的容量,否则即使能够复用,也始终有一部分内存空间是浪费掉的。

参考资料

[1]

:0]`。但对于一个 map 来说,就没这么简单了。虽然官方对清空 map 也进行了一些优化:[这里: https://github.com/golang/go/issues/20138

[2]

这里: https://go-review.googlesource.com/c/go/+/110055/

[3]

这里: https://cbsheng.github.io/posts/fasthttp源码最佳实践分析/

[4]

这里: https://www.zhihu.com/question/327580797

[5]

这位: https://www.zhihu.com/people/xtlisk

[6]

writev 的 leak: https://github.com/golang/go/pull/32138

推荐阅读

  • 线上真实场景:实战 Goroutine 泄露排查


喜欢本文的朋友,欢迎关注“Go语言中文网”:

58135e746470e33fea41b455515857d7.png

Go语言中文网启用微信学习交流群,欢迎加微信:274768166,投稿亦欢迎

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值