package main
import (
"fmt"
"time"
)
func main() {
storage := []string{}
for i := 0; i < 50000000; i++ {
storage = append(storage, "string string string string string string string string string string string string")
}
fmt.Println("done allocating, emptying")
storage = storage[:0]
storage = nil
for {
time.Sleep(1 * time.Second)
}
}
Run Code Online (Sandbox Code Playgroud)
上面的代码将分配大约 30mb 的内存,然后不会释放它。这是为什么?如何强制释放此切片使用的内存?我把那片切成薄片,然后把它磨碎了。
我正在调试的程序是一个简单的 HTTP 输入缓冲区:它将所有请求附加到大块中,并通过通道将这些块发送到 goroutine 进行处理。但问题如上所示 - 我无法通过存储来释放内存,然后最终耗尽内存。
编辑:正如有些人对类似问题指出的那样,不,它首先不起作用,其次不是我所要求的。切片被清空,内存不会。
这里有几件事情正在发生。
第一个需要吸收的是,Go 是一种垃圾收集语言;它的 GC 的实际算法大多是无关紧要的,但理解它的一个方面至关重要:它不使用引用计数,因此无法以某种方式使 GC 立即回收任何给定值的内存,这些值的存储分配在堆。用更简单的话概括一下,这样做是徒劳的
s := make([]string, 10*100*100)
s = nil
Run Code Online (Sandbox Code Playgroud)
因为第二条语句确实会删除对切片底层内存的唯一引用,但不会使 GC 运行并将该内存“标记”为可重用。
这意味着两件事:
后者可以通过多种方式完成:
预分配,当您对分配多少有一个合理的想法时。
在您的示例中,您从长度为 0 的切片开始,然后向其追加很多内容。现在,几乎所有处理内存缓冲区增长的库代码(包括 Go 运行时)都通过 1) 分配两倍请求的内存来处理这些分配——希望防止未来多次分配,以及 2)复制“旧”内容,当它必须重新分配。这一点很重要:当重新分配发生时,这意味着现在有两个内存区域:旧的和新的。
如果您可以估计您可能需要N平均保存元素,请使用make([]T, 0, N)-此处
和此处的更多信息为它们预分配。如果您需要持有少于N元素,则该缓冲区的尾部将不被使用,如果您需要持有多于N,则需要重新分配,但平均而言,您不需要任何重新分配。
重新使用您的切片。说,在您的情况下,您可以通过将切片重新切片为零长度来“重置”切片,然后再次将其用于下一个请求。这称为“池化”,在对此类池进行大规模并行访问的情况下,您可以使用它sync.Pool来保存缓冲区。
限制系统上的负载,使 GC 能够应对持续的负载。两个很好的概述接近这样的限制是这个。