가비지 수집은 Go의 매우 편리한 기능입니다. 자동 메모리 관리로 인해 코드가 더 깔끔해지고 메모리 누수 가능성이 줄어듭니다. 그러나 가비지 컬렉션은 사용하지 않는 객체를 수집하기 위해 프로그램을 주기적으로 중지해야 하기 때문에 필연적으로 추가 오버헤드가 추가됩니다. Go 컴파일러는 똑똑하며 나중에 쉽게 수집할 수 있도록 변수를 힙에 할당해야 하는지, 아니면 함수의 스택 공간에 직접 할당해야 하는지 자동으로 결정합니다. 스택에 할당된 변수의 경우 힙에 할당된 변수와 다른 점은 함수가 반환되면 스택 공간이 소멸되므로 추가적인 가비지 수집 오버헤드 없이 스택의 변수가 직접 소멸된다는 점입니다.
Go의 탈출 분석은 Java 가상 머신의 HotSpot보다 더 기본적입니다. 기본 규칙은 변수에 대한 참조가 선언된 함수에서 반환되면 함수 외부의 다른 콘텐츠에서 사용될 수 있으므로 "이스케이프"가 발생한다는 것입니다. 다음 상황은 더욱 복잡해집니다.
- 다른 함수를 호출하는 함수
- 멤버 변수를 구조로 참조
- 슬라이싱 및 매핑
- 변수에 대한 Cgo 포인터
이스케이프 분석을 구현하기 위해 Go는 컴파일 단계에서 구성합니다. 입력 매개변수 및 반환 값의 프로세스를 추적하는 함수 호출 다이어그램. 함수가 매개변수만 참조하고 참조가 함수를 반환하지 않는 경우 변수는 이스케이프되지 않습니다. 함수가 참조를 반환하지만 스택의 다른 함수에 의해 참조가 해제되거나 참조를 반환하지 않는 경우 이스케이프가 없습니다. 여러 예를 보여주기 위해 컴파일할 때 -gcflags '-m'
매개변수를 추가할 수 있습니다. 이 매개변수는 이탈 분석에 대한 자세한 정보를 인쇄합니다. -gcflags '-m'
参数,这个参数会打印逃逸分析的详细信息:
package main type S struct {} func main() { var x S _ = identity(x) } func identity(x S) S { return x }
你可以执行go run -gcflags '-m -l'
(注:原文中略了go代码文件名)来编译这个代码,-l参数是防止函数identity
被内联(换个时间再讨论内联这个话题)。你将会看到没有任何输出!Go使用值传递,所以main
函数中的x
这个变量总是会被拷贝到函数identity
的栈空间。通常情况下没有使用引用的代码都是通过栈空间来分配内存。所以不涉及逃逸分析。下面试下困难一点的:
package main type S struct {} func main() { var x S y := &x _ = *identity(y) } func identity(z *S) *S { return z }
其对应的输出是:
./escape.go:11: leaking param: z to result ~r1 ./escape.go:7: main &x does not escape
第一行显示了变量z
的“流经”:入参直接作为返回值返回了。但是函数identity
没有取走z
这个引用,所以没有发生变量逃逸。在main
函数返回后没有任何对x
的引用存在,所以x
这个变量可以在main
函数的栈空间进行内存分配。
第三次实验:
package main type S struct {} func main() { var x S _ = *ref(x) } func ref(z S) *S { return &z }
其输出为:
./escape.go:10: moved to heap: z ./escape.go:11: &z escapes to heap
现在有了逃逸发生。记住Go是值传递的,所以z
是对变量x
的一个拷贝。函数ref
返回一个对z
的引用,所以z
不能在栈中分配,否则当函数ref
返回时,引用会指向何处呢?于是它逃逸到了堆中。其实执行完ref
返回到main
函数中后,main
函数丢弃了这个引用而不是解除引用,但是Go的逃逸分析还不够机智去识别这种情况。
值得注意的是,在这种情况下,如果我们不停止引用,编译器将内联ref
。
如果结构体成员定义的是引用又会怎样呢?
package main type S struct { M *int } func main() { var i int refStruct(i) } func refStruct(y int) (z S) { z.M = &y return z }
其输出为:
./escape.go:12: moved to heap: y ./escape.go:13: &y escapes to heap
在这种情况下,尽管引用是结构体的成员,但Go仍然会跟踪引用的流向。由于函数refStruct
接受引用并将其返回,因此y
必须逃逸。对比如下这个例子:
package main type S struct { M *int } func main() { var i int refStruct(&i) } func refStruct(y *int) (z S) { z.M = y return z }
其输出为:
./escape.go:12: leaking param: y to result z ./escape.go:9: main &i does not escape
尽管在main
函数中对i
变量做了引用操作,并传递到了函数refStruct
中,但是这个引用的范围没有超出其声明它的栈空间。这和之前的那个程序语义上有细微的差别,这个会更高效:在上一个程序中,变量i
必须分配在main
函数的栈中,然后作为参数拷贝到函数refStruct
中,并将拷贝的这一份分配在堆上。而在这个例子中,i
package main type S struct { M *int } func main() { var x S var i int ref(&i, &x) } func ref(y *int, z *S) { z.M = y }
를 실행하여 실행할 수 있습니다. -gcflags ' -m -l'
(참고: 원본 텍스트에서는 go 코드 파일 이름이 생략됨) -l 매개변수는 identity
함수가 인라인되는 것을 방지합니다. (인라인화에 대해서는 다른 시간에 논의하겠습니다) 주제). 출력이 표시되지 않습니다! Go는 값 전송을 사용하므로 main
함수의 변수 x
는 항상 identity
함수의 스택 공간에 복사됩니다. 일반적으로 참조를 사용하지 않는 코드는 스택 공간을 통해 메모리를 할당합니다. 따라서 탈출 분석은 포함되지 않습니다. 더 어려운 것을 시도해 보겠습니다.
./escape.go:13: leaking param: y ./escape.go:13: ref z does not escape ./escape.go:9: moved to heap: i ./escape.go:10: &i escapes to heap ./escape.go:10: main &x does not escape해당 출력은 다음과 같습니다.
rrreee
첫 번째 줄은 변수z
의 "흐름"을 보여줍니다. 입력 매개변수는 반환 값으로 직접 반환됩니다. 그러나 identity
함수는 z
의 참조를 제거하지 않았으므로 변수 이스케이프가 발생하지 않았습니다. main
함수가 반환된 후에는 x
에 대한 참조가 없으므로 x
변수가 main
에 있을 수 있습니다. 함수. 메모리 할당을 위한 스택 공간입니다. 🎜세 번째 실험: 🎜rrreee🎜출력은 다음과 같습니다. 🎜rrreee🎜이제 탈출구가 있습니다. Go는 값별 전달이므로 z
는 변수 x
의 복사본입니다. ref
함수는 z
에 대한 참조를 반환하므로 z
는 스택에 할당될 수 없습니다. 그렇지 않으면 ref
함수가 > 반환 When , 기준점은 어디입니까? 그래서 힙으로 탈출했습니다. 실제로 ref
를 실행하고 main
함수로 돌아온 후 main
함수는 참조를 역참조하는 대신 참조를 삭제하지만 Go의 이스케이프 분석은 다음과 같습니다. 충분하지 않습니다. 이 상황을 인식하려면 재치를 사용하십시오. 🎜이 경우 참조를 중지하지 않으면 컴파일러가 ref
를 인라인한다는 점에 주목할 가치가 있습니다. 🎜구조체 멤버가 참조로 정의되면 어떻게 되나요? 🎜rrreee🎜출력은 다음과 같습니다. 🎜rrreee🎜이 경우 Go는 구조체의 멤버임에도 불구하고 참조의 흐름을 계속 추적합니다. refStruct
함수는 참조를 받아 반환하므로 y
는 이스케이프해야 합니다. 다음 예를 비교해 보세요. 🎜rrreee🎜출력은 다음과 같습니다. 🎜rrreee🎜i
변수는 main
함수에서 참조되고 refStruct 함수에 전달됩니다. code>이지만 이 참조의 범위는 선언된 스택 공간을 초과하지 않습니다. 이는 이전 프로그램과 의미가 약간 다르며 더 효율적입니다. 이전 프로그램에서 변수 <code>i
는 main
의 스택에 할당되어야 합니다. 함수를 선택한 다음 refStruct
함수에 매개변수로 복사하고 힙에 복사본을 할당합니다. 이 예에서는 i
가 한 번만 할당된 다음 참조가 전달됩니다. 🎜🎜다소 복잡한 예를 살펴보겠습니다. 🎜rrreee🎜출력은 다음과 같습니다. 🎜./escape.go:13: leaking param: y ./escape.go:13: ref z does not escape ./escape.go:9: moved to heap: i ./escape.go:10: &i escapes to heap ./escape.go:10: main &x does not escape
问题在于,y
被赋值给了一个入参结构体的成员。Go并不能追溯这种关系(go只能追溯输入直接流向输出),所以逃逸分析失败了,所以变量只能分配到堆上。由于Go的逃逸分析的局限性,许多变量会被分配到堆上,请参考此链接,这里面记录了许多案例(从Go1.5开始)。
最后,来看下映射和切片是怎样的呢?请记住,切片和映射实际上只是具有指向堆内存的指针的Go结构:slice
结构是暴露在reflect
包中(SliceHeader
)。map
结构就更隐蔽了:存在于hmap。如果这些结构体不逃逸,将会被分配到栈上,但是其底层的数组或者哈希桶中的实际数据会被分配到堆上去。避免这种情况的唯一方法是分配一个固定大小的数组(例如[10000]int
)。
如果你剖析过你的程序堆使用情况(https://blog.golang.org/pprof
),并且想减少垃圾回收的消耗,可以将频繁分配到堆上的变量移到栈上,可能会有较好的效果。进一步研究HotSpot JVM是如何进行逃逸分析的会是一个不错的话题,可以参考这个链接,这个里面主要讲解了栈分配,以及有关何时可以消除同步的检测。
更多golang相关技术文章,请访问golang教程栏目!