如果我实现这样的队列......
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的垃圾收集器是否足够智能以释放它们?
答案 0 :(得分:21)
切片只是描述符(类似于结构的小数据结构),如果没有引用,将被正确地进行垃圾收集。
另一方面,切片的基础数组(描述符所指向的)在通过重新创建它的所有切片之间共享:引自Go Language Specification: Slice Types:< / p>
切片一旦初始化,就始终与保存其元素的基础数组相关联。因此,切片与其阵列和同一阵列的其他切片共享存储;相比之下,不同的数组总是代表不同的存储。
因此,如果存在至少一个切片,或者保存数组的变量(如果通过切片数组创建切片),则不会进行垃圾回收。
关于此的正式声明:
博客文章Go Slices: usage and internals Andrew Gerrand清楚地说明了这种行为:
如前所述,对切片进行重新切片并不会复制底层数组。 完整数组将保留在内存中,直到不再引用它为止。有时这会导致程序在只需要一小部分数据时将所有数据保存在内存中。
...
由于切片引用原始数组,只要切片保留在垃圾收集器周围就不能释放数组。
返回您的示例
虽然不会释放底层数组,但请注意,如果向队列添加新元素,内置append
函数有时可能会分配一个新数组并将当前元素复制到新数组 - 但是复制将只复制切片的元素而不是整个底层数组!当这样的重新分配和复制发生时,&#34; old&#34;如果没有其他引用,则可能会对数组进行垃圾回收。
另一个非常重要的事情是,如果从前面弹出一个元素,那么切片将被重新复制并且不包含对弹出元素的引用,但由于底层数组仍包含该值,因此该值也将保留在内存(不仅仅是数组)。建议无论何时从队列中弹出或删除元素(切片/数组),始终将其归零(其在切片中的相应元素),因此该值将不会不必要地保留在内存中。如果切片包含指向大数据结构的指针,这就变得更加重要。
func PopFront(q *[]string) string {
r := (*q)[0]
(*q)[0] = "" // Always zero the removed element!
*q = (*q)[1:len(*q)]
return r
}
删除而不保留订单
a[i] = a[len(a)-1] a = a[:len(a)-1]
注意如果元素的类型是指针或带有指针字段的结构,需要进行垃圾回收,则
Cut
的上述实现并且Delete
有潜在的内存泄漏问题:某些带有值的元素仍然被切片a
引用,因此无法收集。
答案 1 :(得分:1)
简单的问题,简单的回答:没有。(但是如果你继续推动切片将在某个时刻溢出其底层数组,那么未使用的元素就可以被释放了。)
答案 2 :(得分:1)
否。在撰写本文时,Go垃圾收集器(GC)不够聪明,无法收集切片中的基础数组的开始,即使无法访问。
正如这里的其他人所提到的,切片(在幕后)是由三件事构成的结构:指向其基础数组的指针,切片的长度(无需重新切片即可访问的值)以及切片的容量(可以通过切片访问的值)。在“围棋”博客上,slice internals are discussed at length。这是我喜欢的另一篇about Go memory layouts。
重新切片并切断切片的尾端时,很明显(通过了解内部原理),底层数组,指向底层数组的指针以及切片的容量都是保持不变;仅切片长度字段被更新。重新切片并剪切切片的开始时,实际上是在更改指向底层数组的指针以及长度和容量。在这种情况下,通常不清楚(根据我的阅读),为什么GC无法清理基础数组的此不可访问部分,因为您无法重新切片该数组以再次访问它。我的假设是,从GC的角度来看,底层数组被视为一块内存。如果您可以指向基础数组的任何部分,则整个对象都无法进行释放。
我知道您在想什么……就像您是真正的计算机科学家一样,您可能需要一些证明。我会纵容你:
https://goplay.space/#tDBQs1DfE2B
正如其他人所提到的,如示例代码所示,使用append
可以导致底层数组的重新分配和复制,从而可以对旧的底层数组进行垃圾回收。
答案 3 :(得分:0)
与我正在阅读的内容相反,Golang当然似乎在垃圾收集至少未使用的切片开始部分。以下测试案例提供了证据。
在第一种情况下,在每次迭代中将slice设置为slice [:1]。在比较的情况下,它跳过该步骤。
第二种情况使第一种情况下消耗的内存相形见war。但是为什么呢?
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)
}
}
}