在C/C++裏面 變量分配在棧和堆是有區別的,簡而言之嗎,通常變量分配在棧上會隨着函數釋放而釋放,變量分配在堆上則不會隨函數返回而釋放。然而在golang裏面這種寫法確是能夠的。
一般在編譯代碼的時候,編譯器根據分析,判斷將變量分配在棧或堆上。函數定義中,通常將局部變量和參數分配到棧上(stack frame)上。可是,若是編譯器不能肯定在函數返回(return)時,變量是否被引用(reference),分配到堆上;若是局部變量很是大,也應分配在堆上。golang
若是對變量取地址(*和&操做),則有可能分配在堆上。此外,還須要進行逃逸分析(escape analytic),判斷return後變量是否被引用,不引用分配到棧上,引用分配到堆上。編程
在golang中逃逸分析是一種肯定指針動態範圍的方法,能夠分析在程序的哪些地方能夠訪問到指針。它涉及到指針分析和形狀分析。當一個變量(或對象)在子程序中被分配時,一個指向變量的指針可能逃逸到其它執行線程中,或者去調用子程序。若是使用尾遞歸優化(一般在函數編程語言中是須要的),對象也可能逃逸到被調用的子程序中。 若是一個子程序分配一個對象並返回一個該對象的指針,該對象可能在程序中的任何一個地方被訪問到——這樣指針就成功「逃逸」了。若是指針存儲在全局變量或者其它數據結構中,它們也可能發生逃逸,這種狀況是當前程序中的指針逃逸。 逃逸分析須要肯定指針全部能夠存儲的地方,保證指針的生命週期只在當前進程或線程中。安全
可是golang 編譯器決定變量應該分配到什麼地方時會進行逃逸分析,下面咱們看段代碼:數據結構
package main編程語言
import ()ide
func foo() *int {
var x int
return &x
}函數
func bar() int {
x := new(int)
*x = 1
return *x
}性能
func main() {}優化
運行後:線程
> go run -gcflags '-m -l' escape.go ./main.go:6: moved to heap: x ./main.go:7: &x escape to heap ./main.go:11: bar new(int) does not escape
foo() 中的 x 最後在堆上分配,而 bar() 中的 x 最後分配在了棧上。在官網 (golang.org) FAQ 上有一個關於變量分配的問題以下:
如何得知變量是分配在棧(stack)上仍是堆(heap)上?
準確地說,你並不須要知道。Golang 中的變量只要被引用就一直會存活,存儲在堆上仍是棧上由內部實現決定而和具體的語法沒有關係。
知道變量的存儲位置確實和效率編程有關係。若是可能,Golang 編譯器會將函數的局部變量分配到函數棧幀(stack frame)上。 然而,若是編譯器不能確保變量在函數 return以後再也不被引用,編譯器就會將變量分配到堆上。並且,若是一個局部變量很是大,那麼它也應該被分配到堆上而不是棧上。
當前狀況下,若是一個變量被取地址,那麼它就有可能被分配到堆上。然而,還要對這些變量作逃逸分析,若是函數return以後,變量再也不被引用,則將其分配到棧上。
其實在golang中全部靜態內存的其實分配都是在 stack 上進行的,而函數體在執行結束出棧後全部在棧上分配的內存都將獲得釋放,若是此時直接返回當前做用域變量的指針,這在下層函數的尋址行爲就會由於出棧的內存釋放而形成空指針異常。這個時候咱們就得須要用到malloc在堆上(heap)動態分配內存,本身管理內存的生命週期,本身手動釋放纔是安全的方式。
然而escape analysis的存在讓go完美規避了這些問題,編譯器在編譯時對代碼作了分析,若是發現當前做用域的變量沒有超出函數範圍,則會自動在stack上分配,若是找不到了,則會在heap上分配。這樣其實開發者就不用太關心堆棧的使用邊界,在代碼層面上徹底不須要關心內存的分配,把底層要考慮的問題交給編譯器,同時也減少了gc回收的壓力。
go在必定程度消除了堆和棧的區別,由於go在編譯的時候進行逃逸分析,來決定一個對象放棧上仍是放堆上,不逃逸的對象放棧上,可能逃逸的放堆上。
那麼逃逸分析的做用是什麼呢?
1.逃逸分析的好處是爲了減小gc的壓力,不逃逸的對象分配在棧上,當函數返回時就回收了資源,不須要gc標記清除。
2.逃逸分析完後能夠肯定哪些變量能夠分配在棧上,棧的分配比堆快,性能好(逃逸的局部變量會在堆上分配 ,而沒有發生逃逸的則有編譯器在棧上分配)。
3.同步消除,若是你定義的對象的方法上有同步鎖,但在運行時,卻只有一個線程在訪問,此時逃逸分析後的機器碼,會去掉同步鎖運行。
咱們在開發的時候其實也能夠本身去設置和查看逃逸分析的log,咱們能夠分析逃逸日誌,只要在編譯的時候加上-gcflags '-m',可是咱們爲了避免讓編譯時自動內連函數,通常會加-l參數,最終爲-gcflags '-m -l'.在main中能夠用:
go run -gcflags '-m -l' main.go
那麼接着就會有個問題何時會逃逸,何時不會逃逸呢?
接下來咱們看個例子:
`package main
type Elegance struct{}
func main() {
var e Elegance
p := &e
_ = *identity(y)
}
func identity(m *Elegance) *Elegance {
return m
}運行後:
main.go:11: leaking param: m to result ~r1 level=0
main.go:7: main &e does not escape`
在這裏的m變量是「流式」,由於identity這個函數僅僅輸入一個變量,又將這個變量做爲返回輸出,但identity並無引用m,因此這個變量沒有逃逸,而e沒有被引用,且生命週期也在mian裏,e沒有逃逸,分配在棧上。
一般在go中函數都是運行在棧上的,在棧聲明臨時變量分配內存,函數運行完畢在回收該段棧空間,而且每一個函數的棧空間都是獨立的,不能被訪問到的。可是在某些狀況下,棧上的空間須要在該函數被釋放後依舊能訪問到,這時候就涉及到內存的逃逸了。
`type data struct {
name string
}
func patent1()data{
p := data{"keke"}
return p
}
func patent2() *data {
p := data{"jame"}
return &p
}
func main(){
p1 := patent1()
p2 := patent2()
}`
這裏的patent1和patent2函數都有返回值,惟一不一樣的地方是patent1返回data結構體,patent2返回data結構體指針。在大多數語言例如C相似patent2的函數是不對的,由於p是一個臨時變量,返回事後就會被釋放掉,返回毫無心義。可是在golang中,這種語法是容許的,它能正確的把p的地址返回到上層調用函數而不被釋放。
這樣該函數在運行完畢後確定是要釋放的,內部分配的臨時內存也要釋放,因此p也應該被釋放。而爲了讓p能被正確返回到上層調用,golang採起了一種內存策略,把p從棧拿到堆的中去,此時p就不會跟隨patent2一同消亡了,這個過程就是逃逸。