【问题标题】:Does go garbage collect parts of slices?go 垃圾会收集部分切片吗?
【发布时间】:2015-04-10 13:02:44
【问题描述】:

如果我实现这样的队列...

package main

import(
    "fmt"
)

func PopFront(q *[]string) string {
    r := (*q)[0]
    *q = (*q)[1:len(*q)]
    return r
}

func PushBack(q *[]string, a string) {
    *q = append(*q, a)
}

func main() {
    q := make([]string, 0)

    PushBack(&q, "A")
    fmt.Println(q)
    PushBack(&q, "B")
    fmt.Println(q)
    PushBack(&q, "C")
    fmt.Println(q)

    PopFront(&q)
    fmt.Println(q)
    PopFront(&q)
    fmt.Println(q)      
}

...我最终得到一个数组["A", "B", "C"],它没有指向前两个元素的切片。由于切片的“开始”指针永远不能递减(AFAIK),因此永远不能访问这些元素。

Go 的垃圾收集器足够聪明以释放它们吗?

【问题讨论】:

    标签: arrays go garbage-collection slice


    【解决方案1】:

    切片只是描述符(类似结构的小型数据结构),如果不被引用,将被正确地回收。

    另一方面,切片(描述符指向的)的底层数组在通过重新切片创建的所有切片之间共享:引用Go Language Specification: Slice Types

    切片一旦被初始化,总是与保存其元素的底层数组相关联。因此,一个切片与其数组以及同一数组的其他切片共享存储空间;相比之下,不同的数组总是代表不同的存储。

    因此,如果至少存在一个切片,或保存数组的变量(如果切片是通过切片数组创建的),则不会被垃圾回收。

    官方声明:

    Andrew Gerrand 的博文Go Slices: usage and internals 明确说明了这种行为:

    如前所述,重新切片切片不会复制底层数组。 整个数组将保存在内存中,直到不再被引用。有时这会导致程序在只需要一小部分时将所有数据保存在内存中。

    ...

    由于切片引用了原始数组,只要切片保留在垃圾收集器周围就无法释放数组

    回到你的例子

    虽然底层数组不会被释放,但请注意,如果您将新元素添加到队列中,内置的 append 函数有时可能会分配一个新数组并将当前元素复制到新数组中——但复制只会复制切片的元素而不是整个底层数组!当发生这种重新分配和复制时,如果不存在其他引用,则“旧”数组可能会被垃圾回收。

    另外一个非常重要的事情是,如果一个元素从前面弹出,切片将被重新切片并且不包含对弹出元素的引用,但是由于底层数组仍然包含该值,因此该值也将保留在内存(不仅仅是数组)。建议无论何时从队列(切片/数组)中弹出或删除元素,始终将其归零(切片中的相应元素),这样该值就不会不必要地保留在内存中。如果您的切片包含指向大数据结构的指针,这将变得更加重要。

    func PopFront(q *[]string) string {
        r := (*q)[0]
        (*q)[0] = ""  // Always zero the removed element!
        *q = (*q)[1:len(*q)]
        return r
    }
    

    这是提到Slice Tricks wiki page:

    删除而不保留​​顺序

    a[i] = a[len(a)-1] 
    a = a[:len(a)-1]
    

    注意 如果元素的类型是pointer或带有指针字段的struct,需要进行垃圾回收,上述Cut和@的实现987654331@ 有一个潜在的内存泄漏问题:一些有值的元素仍然被切片a 引用,因此无法被收集。

    【讨论】:

    • 要完成 icza 答案,并针对您的特定队列情况:切片的不可到达部分不会被垃圾收集,但是,当您 PushBack 一个新项目和 q 已经是完整时,将分配一个新的q 副本,在这种情况下,只有可到达的元素将被复制,因此 A、B 和 C 不会。 q 不会随着无法访问的元素而永远增长。
    • @siritinga 这是真的,很好的建议。谢谢。将其与其他重要信息结合到答案中(例如,如果删除了元素,则将“槽”归零)。
    • @icza 如果您有时间,请您对这个问题stackoverflow.com/questions/51959783/… 提供一个解释性答案。
    • 我还要添加一个指向"slice tircks" wiki 页面的链接,尤其是this bit 处理因重新切片而“框出”的元素。
    • @himanshu219 对,只有当它引用存储在切片的后备数组之外的内存时才需要清除该值,例如指针或字符串。
    【解决方案2】:

    。在撰写本文时,Go 垃圾收集器 (GC) 还不够聪明,无法收集切片中底层数组的开头,即使它不可访问

    正如其他人在这里所提到的,切片(在引擎盖下)是一个包含三件事的结构:指向其底层数组的指针、切片的长度(无需重新切片即可访问的值)和切片的容量(可通过重新切片访问的值)。在 Go 博客上,slice internals are discussed at length。这是我喜欢的另一篇文章about Go memory layouts

    当你重新切片并切断切片的尾端时,很明显(在了解内部原理后)底层数组、指向底层数组的指针和切片的容量都是保持不变;仅更新切片长度字段。当您重新切片并切断切片的开始时,您实际上是在更改指向底层数组的指针以及长度和容量。在这种情况下,通常不清楚(根据我的读数)为什么 GC 不清理底层数组的这个不可访问的部分,因为您无法重新切片数组以再次访问它。我的假设是,从 GC 的角度来看,底层数组被视为一块内存。如果您可以指向底层数组的任何部分,则整个事物都不符合解除分配的条件。

    我知道你在想什么……就像你是真正的计算机科学家一样,你可能需要一些证据。我会放纵你的:

    https://goplay.space/#tDBQs1DfE2B

    正如其他人所提到的和示例代码中所示,使用append 可能会导致底层数组的重新分配和复制,从而允许对旧的底层数组进行垃圾回收。

    【讨论】:

      【解决方案3】:

      简单的问题,简单的答案:不。(但是如果你继续推动切片将在某个时候溢出其底层数组,那么未使用的元素将可以被释放。)

      【讨论】:

        【解决方案4】:

        与我正在阅读的内容相反,Golang 似乎肯定会垃圾收集至少未使用的切片开始部分。以下测试用例提供了证据。

        在第一种情况下,切片在每次迭代中都设置为 slice[:1]。在比较情况下,它会跳过该步骤。

        第二种情况使第一种情况消耗的内存相形见绌。但为什么呢?

        func TestArrayShiftMem(t *testing.T) {
            slice := [][1024]byte{}
        
            mem := runtime.MemStats{}
            mem2 := runtime.MemStats{}
            runtime.GC()
            runtime.ReadMemStats(&mem)
        
            for i := 0; i < 1024*1024*1024*1024; i++ {
                slice = append(slice, [1024]byte{})
                slice = slice[1:]
                runtime.GC()
        
                if i%(1024) == 0 {
                    runtime.ReadMemStats(&mem2)
                    fmt.Println(mem2.HeapInuse - mem.HeapInuse)
                    fmt.Println(mem2.StackInuse - mem.StackInuse)
                    fmt.Println(mem2.HeapAlloc - mem.HeapAlloc)
        
                }
            }
        }
        
        
        func TestArrayShiftMem3(t *testing.T) {
            slice := [][1024]byte{}
        
            mem := runtime.MemStats{}
            mem2 := runtime.MemStats{}
            runtime.GC()
            runtime.ReadMemStats(&mem)
        
            for i := 0; i < 1024*1024*1024*1024; i++ {
                slice = append(slice, [1024]byte{})
                // slice = slice[1:]
                runtime.GC()
        
                if i%(1024) == 0 {
                    runtime.ReadMemStats(&mem2)
                    fmt.Println(mem2.HeapInuse - mem.HeapInuse)
                    fmt.Println(mem2.StackInuse - mem.StackInuse)
                    fmt.Println(mem2.HeapAlloc - mem.HeapAlloc)
        
                }
            }
        }
        

        输出测试1:

        go test -run=.Mem -v .
        ...
        0
        393216
        21472
        ^CFAIL  github.com/ds0nt/cs-mind-grind/arrays   1.931s
        

        输出测试3:

        go test -run=.Mem3 -v .
        ...
        19193856
        393216
        19213888
        ^CFAIL  github.com/ds0nt/cs-mind-grind/arrays   2.175s
        

        如果您在第一次测试时禁用垃圾收集,内存确实会飙升。生成的代码如下所示:

        func TestArrayShiftMem2(t *testing.T) {
            debug.SetGCPercent(-1)
        
            slice := [][1024]byte{}
        
            mem := runtime.MemStats{}
            mem2 := runtime.MemStats{}
            runtime.GC()
            runtime.ReadMemStats(&mem)
            // 1kb per
        
            for i := 0; i < 1024*1024*1024*1024; i++ {
                slice = append(slice, [1024]byte{})
                slice = slice[1:]
                // runtime.GC()
        
                if i%(1024) == 0 {
                    fmt.Println("len, cap:", len(slice), cap(slice))
                    runtime.ReadMemStats(&mem2)
                    fmt.Println(mem2.HeapInuse - mem.HeapInuse)
                    fmt.Println(mem2.StackInuse - mem.StackInuse)
                    fmt.Println(mem2.HeapAlloc - mem.HeapAlloc)
        
                }
            }
        } 
        

        【讨论】:

        • append() 有时会分配一个全新的数组,所以我认为这并不能说明您的想法。
        猜你喜欢
        • 2018-10-05
        • 1970-01-01
        • 1970-01-01
        • 2013-05-11
        • 2014-02-23
        • 2015-05-04
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多