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

6
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)
    }
}

上面的代码将分配大约30MB的内存,然后不会释放它。为什么会这样?我该如何强制Go释放这个切片使用的内存?我对该切片进行了切片并将其设置为nil。
我正在调试的程序是一个简单的HTTP输入缓冲区:它将所有请求附加到大块中,并将这些块发送到用于处理的goroutine通道。但上述问题说明了 - 我无法使存储释放内存,最终会耗尽内存。
编辑:正如一些人指出类似问题的文章,首先不起作用,第二个也不是我要求的。切片被清空,但内存没有被释放。

不,这不是我要找的。我可以清空切片,但我需要释放内存。 - Aleksandr Makov
@Filmzy,谢谢。我已经更新了我的问题。 - Aleksandr Makov
你所说的“释放内存”是什么意思?如果你的意思是将内存交还给操作系统,通常不会这样做。如果你的意思是使内存可用于程序中的其他分配,那么是可以这样做的。 - Art
我运行了你的程序大约5到10分钟,内存被释放回操作系统。当我在最后一个for循环之前插入debug.FreeOSMemory()调用时,内存立即被释放回操作系统,无需等待。因此这是Cannot free memory once occupied by bytes.Buffer问题的重复。 - icza
2个回答

13

这里有几件事情需要注意。

首先需要理解的是,Go是一种垃圾回收语言;其实际的GC算法大多数都不重要,但其中一个方面至关重要:它不使用引用计数,因此没有办法让GC立即回收在堆上分配存储空间的任何给定值的内存。 简单来说,这样做是徒劳的。

s := make([]string, 10*100*100)
s = nil

由于第二个语句确实会删除切片底层内存的唯一引用,但它不会让垃圾回收器标记该内存可供重用。

这意味着两件事情:

  • 你应该知道垃圾回收器是如何工作的。这里介绍了自v1.5以来(直到现在v1.10)它的工作原理。
  • 你应该以减少内存压力的方式构建那些占用大量内存的算法。

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

  • 在你有一个合理的想法时进行预分配。

    在你的示例中,你从长度为0的切片开始,然后大量添加。现在,几乎所有处理增长型内存缓冲区的库代码(包括Go运行时)都通过以下方式处理这些分配:1)分配比请求的内存多两倍 - 希望能够防止未来的多次分配,并且2)复制“旧”内容,当需要重新分配时。这一点很重要:当重新分配发生时,意味着现在有两个内存区域:旧的和新的。

    如果你能估计平均需要保持N个元素,就使用make([]T,0,N)进行预分配 - 有关更多信息,请点击这里这里。如果你需要保持的元素少于N个,则该缓冲区的尾部将未使用;如果你需要保持的元素超过N个,则需要重新分配,但平均而言,不需要任何重新分配。

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

  • 限制你的系统负载,以使垃圾回收器能够应对持续负载。关于这种限制的两种方法的良好概述,请点击这里


这是令人难以置信的信息,谢谢。 - Aleksandr Makov
@AleksandrMakov,预分配示例中出现了错误——应该是make([]T, 0, N),抱歉。在文档中添加了更多关于切片工作原理的指针。 - kostix

2

在你编写的程序中,释放内存毫无意义,因为代码的任何部分都不再请求它。

要提出一个有效的案例,你需要在循环内请求新的内存并释放它。然后你会观察到内存消耗将在某个点稳定下来。


好的,是的,这当然不是实际的程序。这是实际代码的一部分:func(ctx *fasthttp.RequestCtx) { uris = append(uris, string(ctx.RequestURI()[:])) } 然后在go例程中 go func() { for { time.Sleep(10 * time.Second) if len(uris) > 0 { metrics <- uris uris = uris[:0] } } }() 在基准测试下,我总是很快就用完了内存。 - Aleksandr Makov
我已经更新了我的问题,使其更加实际。 - Aleksandr Makov
1
@AleksandrMakov 现在的回答没有意义,请再提出一个问题。 - Grzegorz Żur

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接