如何释放切片分配的内存?

Ale*_*kov 4 go

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 进行处理。但问题如上所示 - 我无法通过存储来释放内存,然后最终耗尽内存。

编辑:正如有些人对类似问题指出的那样,不,它首先不起作用,其次不是我所要求的。切片被清空,内存不会。

kos*_*tix 8

这里有几件事情正在发生。

第一个需要吸收的是,Go 是一种垃圾收集语言;它的 GC 的实际算法大多是无关紧要的,但理解它的一个方面至关重要:它不使用引用计数,因此无法以某种方式使 GC 立即回收任何给定值的内存,这些值的存储分配在堆。用更简单的话概括一下,这样做是徒劳的

s := make([]string, 10*100*100)
s = nil
Run Code Online (Sandbox Code Playgroud)

因为第二条语句确实会删除对切片底层内存的唯一引用,但不会使 GC 运行并将该内存“标记”为可重用。

这意味着两件事:

  • 您应该知道 GC 是如何工作的。 解释了从 v1.5 到现在(现在是 v1.10)它是如何工作的。
  • 您应该以减少内存压力的方式构建那些占用大量内存的算法。

后者可以通过多种方式完成:

  • 预分配,当您对分配多少有一个合理的想法时。

    在您的示例中,您从长度为 0 的切片开始,然后向其追加很多内容。现在,几乎所有处理内存缓冲区增长的库代码(包括 Go 运行时)都通过 1) 分配两倍请求的内存来处理这些分配——希望防止未来多次分配,以及 2)复制“旧”内容,当它必须重新分配。这一点很重要:当重新分配发生时,这意味着现在有两个内存区域:旧的和新的。

    如果您可以估计您可能需要N平均保存元素,请使用make([]T, 0, N)-此处此处的更多信息为它们预分配。如果您需要持有少于N元素,则该缓冲区的尾部将不被使用,如果您需要持有多于N,则需要重新分配,但平均而言,您不需要任何重新分配。

  • 重新使用您的切片。说,在您的情况下,您可以通过将切片重新切片为零长度来“重置”切片,然后再次将其用于下一个请求。这称为“池化”,在对此类池进行大规模并行访问的情况下,您可以使用它sync.Pool来保存缓冲区。

  • 限制系统上的负载,使 GC 能够应对持续的负载。两个很好的概述接近这样的限制是这个