前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >Go常见错误集锦之切片使用不当会造成内存泄漏的那些场景

Go常见错误集锦之切片使用不当会造成内存泄漏的那些场景

作者头像
Go学堂
发布2023-01-31 15:37:09
发布2023-01-31 15:37:09
1.2K10
代码可运行
举报
文章被收录于专栏:Go工具箱Go工具箱
运行总次数:0
代码可运行

某些情况下,对一个已存在的切片或数组进行切分操作可能会导致内存泄漏。本文我们将介绍导致内存泄漏的场景以及如何避免内存泄漏。

01 因切片容量而导致内存泄漏

假设我们有一个二进制的协议。该协议使用前5个字节标识消息类型。我们基于该协议接收一个很大的消息,同时我们会将最近收到的1000条消息的类型存储在内存中,即存储在一个切片中(例如,出于校验目的)。我们的函数的大体结构如下:

代码语言:javascript
代码运行次数:0
运行
复制
func consumeMessages() {
    for {
      msg := receiveMessage() ①
      storeMessageType(getMessageType(msg)) ②
      // Do something with msg
    }
}

func getMessageType(msg []byte) []byte { ③
    return msg[:5]
}

① 接收一个[]byte消息并赋值给msg变量

② 存储消息类型

③ 通过切分msg切片计算消息类型

storeMessageType函数将存储最近的1000条消息的消息类型(1000个字节类型的切片)。然后测试该实现,功能正常。然后,我们将其部署到生产环境下,然而我们观察到在生产环境的大流量下会消耗很大的内存。这是为什么呢?

当我们使用msg[:5]对msg进行切分操作时,实际上是创建了一个长度为5的新切片。因为新切片和原切片共享同一个底层数据。所以它的容量依然是跟源切片msg的容量一样。即使实际的msg不再被引用,但剩余的元素依然在内存中。下图演示了一个msg接收了一个拥有100万个元素的示例:

正如我们在图中看到的,下一次迭代后,虽然msg被重新赋值了,但原来的切片的底层数组依然是100万字节。虽然我们只想存储每个消息的前5字节代表的消息类型(即5*1000个字节),但同时我们将每条消息的整个容量的数据也存储在了内存中。

那么,我们该如何解决呢?最简单的方法就是在getMessageType函数内部将消息类型拷贝到一个新的切片上,来替代对msg进行切分:

代码语言:javascript
代码运行次数:0
运行
复制
func getMessageType(msg []byte) []byte {
    msgType := make([]byte, 5)
    copy(msgType, msg)
    return msgType
}

msgType是一个5字节的切片。该实现是通过内建函数copy将元素复制到目标切片中。因为该函数只拷贝min(len(dst), len(src))个元素到目标切片中。同时新切片的容量又等于切片长度。因此,无论接收到的消息是多少大,我们只存储了5个元素**。

总之,在我们刚才的应用程序中,对一个已存在的切片或数组进行切分,本质上是创建了一个底层数组和源切片一样大小的新的切片,从而导致了高内存消耗。使用内建的copy函数,可以按实际需要控制消耗的内存。

02 因指针类型导致内存泄露


在上一节我们了解到,对一个已有的切片进行切分操作,由于新切分的切片的容量和原有的切片的容量是一样的,所以原有的元素依然存储在内存中。

那么,在内存中元素会被GC回收吗?

下面的示例函数keepFirstElementOnly是只返回切片的第一个元素:

代码语言:javascript
代码运行次数:0
运行
复制
func keepFirstElementOnly(ids []string) []string {
      return string[:1]
}

如果我们传递给keepFirstElementOnly函数一个有100个字符串的切片,那么,剩下的99个字符串会被GC回收吗?在该例子中是会被回收的。容量将保持为100个元素,但会收集剩余的99个字符串将减少所消耗的内存。

现在,我们通过指针的方式传递元素,看看会发生什么:

代码语言:javascript
代码运行次数:0
运行
复制
func keepFirstElementOnly(ids []*string) []*string {
        return customers[:1]
}

现在剩余的99个元素还会被GC回收吗?在该示例中是不可以的。

规则如下:若切片的元素类型是指针或带指针字段的结构体,那么元素将不会被GC回收。如果我们想返回一个容量为1的切片,我们可以使用copy函数或使用满切片表达式(s[:1:1])。另外,如果我们想保持容量,则需要将剩余的元素填充为nil:

代码语言:javascript
代码运行次数:0
运行
复制
func keepFirstElementOnly(ids []*string) []*string {
  for i := 1; i < len(ids); i++ {
    ids[i] = nil
  }
  return ids[:1]
}

对于剩余所有的元素,我们手动的填充为nil。在本示例中,我们会返回一个具有和输入参数切片的容量大小一致的切片,但剩下的 *string类型的元素会被GC自动回收。

03 小结


本节中,我们看到了两种潜在的内存泄露问题。第一种是关于在已有的切片或数组上进行切分操作而保留了原有切片的容量大小导致内存泄露。如果我们在一个大的切片上只切分出一个小的切片,那么大量内存将会保持分配状态但没有得到应用。第二种是当我们在切分一个元素类型为指针类型的切片或切片的类型是含有指针字段的结构体时,GC不会自动回收这些元素。在我们列举的例子中,我们通过将剩余元素手动置为nil已达到自动回收的目的。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-10-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Go学堂 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 某些情况下,对一个已存在的切片或数组进行切分操作可能会导致内存泄漏。本文我们将介绍导致内存泄漏的场景以及如何避免内存泄漏。
    • 01 因切片容量而导致内存泄漏
    • 假设我们有一个二进制的协议。该协议使用前5个字节标识消息类型。我们基于该协议接收一个很大的消息,同时我们会将最近收到的1000条消息的类型存储在内存中,即存储在一个切片中(例如,出于校验目的)。我们的函数的大体结构如下:
    • 02 因指针类型导致内存泄露
    • 03 小结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档