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进行切分:
func getMessageType(msg []byte) []byte {
msgType := make([]byte, 5)
copy(msgType, msg)
return msgType
}
msgType是一个5字节的切片。该实现是通过内建函数copy将元素复制到目标切片中。因为该函数只拷贝min(len(dst), len(src))个元素到目标切片中。同时新切片的容量又等于切片长度。因此,无论接收到的消息是多少大,我们只存储了5个元素**。
总之,在我们刚才的应用程序中,对一个已存在的切片或数组进行切分,本质上是创建了一个底层数组和源切片一样大小的新的切片,从而导致了高内存消耗。使用内建的copy函数,可以按实际需要控制消耗的内存。
在上一节我们了解到,对一个已有的切片进行切分操作,由于新切分的切片的容量和原有的切片的容量是一样的,所以原有的元素依然存储在内存中。
那么,在内存中元素会被GC回收吗?
下面的示例函数keepFirstElementOnly是只返回切片的第一个元素:
func keepFirstElementOnly(ids []string) []string {
return string[:1]
}
如果我们传递给keepFirstElementOnly函数一个有100个字符串的切片,那么,剩下的99个字符串会被GC回收吗?在该例子中是会被回收的。容量将保持为100个元素,但会收集剩余的99个字符串将减少所消耗的内存。
现在,我们通过指针的方式传递元素,看看会发生什么:
func keepFirstElementOnly(ids []*string) []*string {
return customers[:1]
}
现在剩余的99个元素还会被GC回收吗?在该示例中是不可以的。
规则如下:若切片的元素类型是指针或带指针字段的结构体,那么元素将不会被GC回收。如果我们想返回一个容量为1的切片,我们可以使用copy函数或使用满切片表达式(s[:1:1])。另外,如果我们想保持容量,则需要将剩余的元素填充为nil:
func keepFirstElementOnly(ids []*string) []*string {
for i := 1; i < len(ids); i++ {
ids[i] = nil
}
return ids[:1]
}
对于剩余所有的元素,我们手动的填充为nil。在本示例中,我们会返回一个具有和输入参数切片的容量大小一致的切片,但剩下的 *string类型的元素会被GC自动回收。
本节中,我们看到了两种潜在的内存泄露问题。第一种是关于在已有的切片或数组上进行切分操作而保留了原有切片的容量大小导致内存泄露。如果我们在一个大的切片上只切分出一个小的切片,那么大量内存将会保持分配状态但没有得到应用。第二种是当我们在切分一个元素类型为指针类型的切片或切片的类型是含有指针字段的结构体时,GC不会自动回收这些元素。在我们列举的例子中,我们通过将剩余元素手动置为nil已达到自动回收的目的。