如果我实现这样的队列…
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),因此永远无法访问这些元素。
["A", "B", "C"]
Go的垃圾收集器足够聪明以释放它们吗?
切片只是描述符(类似于小型结构的数据结构),如果不对其进行引用,则会对其进行正确的垃圾回收。
另一方面,切片的基本数组(描述符指向该数组)在所有切片之间 共享 ,这些切片通过切片来 共享 :引用自Go语言规范:Slice Types:
切片一旦初始化,便始终与包含其元素的基础数组关联。因此,一个片与其阵列以及同一阵列的其他片共享存储。相反,不同的数组始终代表不同的存储。
因此,如果存在至少一个片,或保存数组的变量(如果通过对数组进行切片来创建片),则不会进行垃圾回收。
关于此的官方声明:
安德鲁·格朗德(Andrew Gerrand)撰写的博客文章 Go Slices:用法和内部 原理明确说明了这种行为:
如前所述,对切片进行重新切片不会复制基础数组。 完整的数组将保留在内存中,直到不再被引用为止。 有时,这可能导致程序仅需要一小部分数据时就将所有数据保存在内存中。 … 由于切片引用了原始数组, 因此只要将切片保留在垃圾收集器周围,就无法释放该数组 。
如前所述,对切片进行重新切片不会复制基础数组。 完整的数组将保留在内存中,直到不再被引用为止。 有时,这可能导致程序仅需要一小部分数据时就将所有数据保存在内存中。
…
由于切片引用了原始数组, 因此只要将切片保留在垃圾收集器周围,就无法释放该数组 。
回到你的例子
虽然基础数组不会被释放,但是请注意,如果您将新元素添加到队列中,则内置append函数有时可能会分配新数组并将当前元素复制到新元素上,但是复制只会复制切片的元素而不是整个基础数组!当发生这种重新分配和复制时,如果没有其他引用,则可能会“回收”旧数组。
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页面:
删除但不保留订单 a[i] = a[len(a)-1] a = a[:len(a)-1] 注意 如果元素的类型是一个 指针 或指针字段,其需要被垃圾收集一个结构,上述实施方式Cut和Delete有潜在的 存储器泄露 的问题:其值的一些元素仍然由切片引用a并因此不能集。
a[i] = a[len(a)-1] a = a[:len(a)-1]
注意 如果元素的类型是一个 指针 或指针字段,其需要被垃圾收集一个结构,上述实施方式Cut和Delete有潜在的 存储器泄露 的问题:其值的一些元素仍然由切片引用a并因此不能集。
Cut
Delete
a