去垃圾收集切片的部分?

Tim*_*mmm 16 arrays garbage-collection go slice

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

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)      
}
Run Code Online (Sandbox Code Playgroud)

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

Go的垃圾收集器是否足够智能以释放它们?

icz*_*cza 23

切片只是描述符(小型结构类数据结构),如果没有引用,将被正确地进行垃圾收集.

另一方面,切片的底层数组(描述符所指向的)在通过复制它创建的所有切片之间共享:引用来自Go语言规范:切片类型:

一旦初始化,切片始终与保存其元素的基础数组相关联.因此,切片与其阵列和同一阵列的其他切片共享存储; 相比之下,不同的数组总是代表不同的存储.

因此,如果存在至少一个切片,或者保存数组的变量(如果通过切片数组创建切片),则不会对其进行垃圾回收.

关于此的正式声明:

博客文章Go Slices:用法和内部作者Andrew Gerrand清楚地说明了这种行为:

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

...

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

回到你的例子

虽然不会释放底层数组,但请注意,如果向队列添加新元素,内置append函数有时可能会分配一个新数组并将当前元素复制到新数组 - 但复制只会复制切片的元素而不是整个底层阵列!当发生这样的重新分配和复制时,如果不存在其他引用,则可以对"旧"数组进行垃圾收集.

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

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

这是提到Slice Tricks wiki页面:

删除而不保留​​订单

a[i] = a[len(a)-1] 
a = a[:len(a)-1]
Run Code Online (Sandbox Code Playgroud)

注意如果元素的类型是指针或带有指针字段的结构,需要进行垃圾回收,上面的实现CutDelete潜在的内存泄漏问题:一些带有值的元素仍然被切片引用a,因此不能集.

  • 要完成icza答案,并针对您的特定队列情况:切片的无法访问的部分将不会被垃圾收集,但是,当您"PushBack"一个新项目并且`q`已经满了时,`q的新副本`将被分配,在这种情况下,**只会复制可到达的元素**,因此A,B和C不会被复制.`q`不会随着无法到达的元素而永远增长. (8认同)

BMi*_*ner 6

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

正如其他人在这里提到的,切片(在底层)是一个由三件事组成的结构:指向其底层数组的指针、切片的长度(无需重新切片即可访问的值)以及切片的容量(可通过重新切片)。在 Go 博客上,详细讨论了切片的内部结构。这是我喜欢的另一篇关于 Go 内存布局的文章。

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

我知道你在想什么...像真正的计算机科学家一样,你可能需要一些证据。我就满足你:

https://goplay.space/#tDBQs1DfE2B

正如其他人提到的以及示例代码中所示,使用append可能会导致底层数组的重新分配和复制,这允许旧的底层数组被垃圾收集。