在講Go的堆棧之前姐呐,先溫習(xí)一下堆棧基礎(chǔ)知識(shí)典蝌。
什么是堆棧曙砂?在計(jì)算機(jī)中堆棧的概念分為:數(shù)據(jù)結(jié)構(gòu)的堆棧和內(nèi)存分配中堆棧。
數(shù)據(jù)結(jié)構(gòu)的堆棧:
堆:堆可以被看成是一棵樹骏掀,如:堆排序鸠澈。在隊(duì)列中,調(diào)度程序反復(fù)提取隊(duì)列中第一個(gè)作業(yè)并運(yùn)行截驮,因?yàn)閷?shí)際情況中某些時(shí)間較短的任務(wù)將等待很長(zhǎng)時(shí)間才能結(jié)束笑陈,或者某些不短小,但具有重要性的作業(yè)侧纯,同樣應(yīng)當(dāng)具有優(yōu)先權(quán)新锈。堆即為解決此類問(wèn)題設(shè)計(jì)的一種數(shù)據(jù)結(jié)構(gòu)甲脏。
棧:一種先進(jìn)后出的數(shù)據(jù)結(jié)構(gòu)眶熬。
這里著重講的是內(nèi)存分配中的堆和棧妹笆。
內(nèi)存分配中的堆和棧
棧(操作系統(tǒng)):由操作系統(tǒng)自動(dòng)分配釋放 ,存放函數(shù)的參數(shù)值娜氏,局部變量的值等拳缠。其操作方式類似于數(shù)據(jù)結(jié)構(gòu)中的棧。
堆(操作系統(tǒng)): 一般由程序員分配釋放贸弥, 若程序員不釋放窟坐,程序結(jié)束時(shí)可能由OS回收,分配方式倒是類似于鏈表绵疲。
堆棧緩存方式
棧使用的是一級(jí)緩存哲鸳, 他們通常都是被調(diào)用時(shí)處于存儲(chǔ)空間中,調(diào)用完畢立即釋放盔憨。
堆則是存放在二級(jí)緩存中徙菠,生命周期由虛擬機(jī)的垃圾回收算法來(lái)決定(并不是一旦成為孤兒對(duì)象就能被回收)。所以調(diào)用這些對(duì)象的速度要相對(duì)來(lái)得低一些郁岩。
堆棧跟蹤
下面討論堆棧跟蹤信息以及如何在堆棧中識(shí)別函數(shù)所傳遞的參數(shù)婿奔。
以下測(cè)試案例的版本是Go 1.11
示例:
package main
import "runtime/debug"
func main() {
slice := make([]string, 2, 4)
Example(slice, "hello", 10)
}
func Example(slice []string, str string, i int) {
debug.PrintStack()
}
列表1是一個(gè)簡(jiǎn)單的程序, main函數(shù)在第5行調(diào)用Example函數(shù)问慎。Example函數(shù)在第9行聲明萍摊,它有三個(gè)參數(shù),一個(gè)字符串slice,一個(gè)字符串和一個(gè)整數(shù)如叼。它的方法體也很簡(jiǎn)單冰木,只有一行,debug.PrintStack()薇正,這會(huì)立即產(chǎn)生一個(gè)堆棧跟蹤信息:
goroutine 1 [running]:
runtime/debug.Stack(0x1, 0x0, 0x0)
C:/Go/src/runtime/debug/stack.go:24 +0xae
runtime/debug.PrintStack()
C:/Go/src/runtime/debug/stack.go:16 +0x29
main.Example(0xc000077f48, 0x2, 0x4, 0x4abd9e, 0x5, 0xa)
D:/gopath/src/example/example/main.go:10 +0x27
main.main()
D:/gopath/src/example/example/main.go:7 +0x79
堆棧跟蹤信息:
第一行顯示運(yùn)行的goroutine是id為 1的goroutine片酝。
第二行 debug.Stack()被調(diào)用
第四行 debug.PrintStack() 被調(diào)用
第六行 調(diào)用debug.PrintStack()的代碼位置,位于main package下的Example函數(shù)挖腰。它也顯示了代碼所在的文件和路徑径荔,以及debug.PrintStack()發(fā)生的行數(shù)(第10行)公浪。
第八行 也調(diào)用Example的函數(shù)的名字,它是main package的main函數(shù)。它也顯示了文件名和路徑什乙,以及調(diào)用Example函數(shù)的行數(shù)。
下面主要分析 傳遞Example函數(shù)傳參信息
// Declaration
main.Example(slice []string, str string, i int)
// Call to Example by main.
slice := make([]string, 2, 4)
Example(slice, "hello", 10)
// Stack trace
main.Example(0x2080c3f50, 0x2, 0x4, 0x425c0, 0x5, 0xa)
上面列舉了Example函數(shù)的聲明深啤,調(diào)用以及傳遞給它的值的信息拴魄。當(dāng)你比較函數(shù)的聲明以及傳遞的值時(shí),發(fā)現(xiàn)它們并不一致崖飘。函數(shù)聲明只接收三個(gè)參數(shù)榴捡,而堆棧中卻顯示6個(gè)16進(jìn)制表示的值。理解這一點(diǎn)的關(guān)鍵是要知道每個(gè)參數(shù)類型的實(shí)現(xiàn)機(jī)制朱浴。
讓我們看第一個(gè)[]string類型的參數(shù)吊圾。slice是引用類型达椰,這意味著那個(gè)值是一個(gè)指針的頭信息(header value),它指向一個(gè)字符串项乒。對(duì)于slice,它的頭是三個(gè)word數(shù)啰劲,指向一個(gè)數(shù)組。因此前三個(gè)值代表這個(gè)slice檀何。
// Slice parameter value
slice := make([]string, 2, 4)
// Slice header values
Pointer: 0xc00006df48
Length: 0x2
Capacity: 0x4
// Declaration
main.Example(slice []string, str string, i int)
// Stack trace
main.Example(0xc00006df48, 0x2, 0x4, 0x4abd9e, 0x5, 0xa)
顯示了0xc00006df48
代表第一個(gè)參數(shù)[]string的指針蝇裤,0x2
代表slice長(zhǎng)度,0x4
代表容量频鉴。這三個(gè)值代表第一個(gè)參數(shù)栓辜。
// String parameter value
“hello”
// String header values
Pointer: 0x4abd9e
Length: 0x5
// Declaration
main.Example(slice []string, str string, i int)
// Stack trace
main.Example(0xc00006df48, 0x2, 0x4, 0x4abd9e, 0x5, 0xa)
顯示堆棧跟蹤信息中的第4個(gè)和第5個(gè)參數(shù)代表字符串的參數(shù)。0x4abd9e
是指向這個(gè)字符串底層數(shù)組的指針垛孔,0x5
是"hello"字符串的長(zhǎng)度啃憎,他們倆作為第二個(gè)參數(shù)。
第三個(gè)參數(shù)是一個(gè)整數(shù)似炎,它是一個(gè)簡(jiǎn)單的word值辛萍。
// Integer parameter value
10
// Integer value
Base 16: 0xa
// Declaration
main.Example(slice []string, str string, i int)
// Stack trace
main.Example(0xc00006df48, 0x2, 0x4, 0x4abd9e, 0x5, 0xa)
顯示堆棧中的最后一個(gè)參數(shù)就是Example聲明中的第三個(gè)參數(shù),它的值是0xa
羡藐,也就是整數(shù)10贩毕。
Methods
接下來(lái)讓我們稍微改動(dòng)一下程序,讓Example變成方法仆嗦。
package main
import (
"fmt"
"runtime/debug"
)
type trace struct{}
func main() {
slice := make([]string, 2, 4)
var t trace
t.Example(slice, "hello", 10)
}
func (t *trace) Example(slice []string, str string, i int) {
fmt.Printf("Receiver Address: %p\n", t)
debug.PrintStack()
}
上例在第8行新增加了一個(gè)類型trace辉阶,在第15將example改變?yōu)閠race的pointer receiver的一個(gè)方法。第12行聲明t的類型為trace瘩扼,第13行調(diào)用它的方法谆甜。
因?yàn)檫@個(gè)方法聲明為pointer receiver的方法,Go使用t的指針來(lái)支持receiver type集绰,即使代碼中使用值來(lái)調(diào)用這個(gè)方法规辱。當(dāng)程序運(yùn)行時(shí),堆棧跟蹤信息如下:
Receiver Address: 0x5781c8
goroutine 1 [running]:
runtime/debug.Stack(0x15, 0xc000071ef0, 0x1)
C:/Go/src/runtime/debug/stack.go:24 +0xae
runtime/debug.PrintStack()
C:/Go/src/runtime/debug/stack.go:16 +0x29
main.(*trace).Example(0x5781c8, 0xc000071f48, 0x2, 0x4, 0x4c04bb, 0x5, 0xa)
D:/gopath/src/example/example/main.go:17 +0x7c
main.main()
D:/gopath/src/example/example/main.go:13 +0x9a
第7行清晰的表明方法的receiver為pointer type栽燕。方法名和報(bào)包名中間有(*trace)罕袋。第二個(gè)值得注意的是堆棧信息中方法的第一個(gè)參數(shù)為receiver的值。方法調(diào)用總是轉(zhuǎn)換成函數(shù)調(diào)用碍岔,并將receiver的值作為函數(shù)的第一個(gè)參數(shù)浴讯。我們可以總堆棧信息中看到實(shí)現(xiàn)的細(xì)節(jié)。
Packing
import (
"runtime/debug"
)
func main() {
Example(true, false, true, 25)
}
func Example(b1, b2, b3 bool, i uint8) {
debug.PrintStack()
}
再次改變Example的方法蔼啦,讓它接收4個(gè)參數(shù)榆纽。前三個(gè)參數(shù)是布爾類型的,第四個(gè)參數(shù)是8bit無(wú)符號(hào)整數(shù)。布爾類型也是8bit表示的奈籽,所以這四個(gè)參數(shù)可以被打包成一個(gè)word亮元,包括32位架構(gòu)和64位架構(gòu)。當(dāng)程序運(yùn)行的時(shí)候唠摹,會(huì)產(chǎn)生有趣的堆棧:
goroutine 1 [running]:
runtime/debug.Stack(0x4, 0xc00007a010, 0xc000077f88)
C:/Go/src/runtime/debug/stack.go:24 +0xae
runtime/debug.PrintStack()
C:/Go/src/runtime/debug/stack.go:16 +0x29
main.Example(0xc019010001)
D:/gopath/src/example/example/main.go:12 +0x27
main.main()
D:/gopath/src/example/example/main.go:8 +0x30
可以看到四個(gè)值被打包成一個(gè)單一的值了0xc019010001
// Parameter values
true, false, true, 25
// Word value
Bits Binary Hex Value
00-07 0000 0001 01 true
08-15 0000 0000 00 false
16-23 0000 0001 01 true
24-31 0001 1001 19 25
// Declaration
main.Example(b1, b2, b3 bool, i uint8)
// Stack trace
main.Example(0x19010001)
顯示了堆棧的值如何和參數(shù)進(jìn)行匹配的。true用1表示奉瘤,占8bit, false用0表示勾拉,占8bit,uint8值25的16進(jìn)制為x19,用8bit表示。我們課喲看到它們是如何表示成一個(gè)word值的盗温。
Go運(yùn)行時(shí)提供了詳細(xì)的信息來(lái)幫助我們調(diào)試程序藕赞。通過(guò)堆棧跟蹤信息stack trace,解碼傳遞個(gè)堆棧中的方法的參數(shù)有助于我們快速定位BUG卖局。
變量是堆(heap)還是堆棧(stack)
寫過(guò)c語(yǔ)言都知道斧蜕,有明確的堆棧和堆的相關(guān)概念。而Go聲明語(yǔ)法并沒(méi)有提到堆椦馀迹或堆批销,只是在Go的FAQ里面有這么一段解釋:
How do I know whether a variable is allocated on the heap or the stack?
From a correctness standpoint, you don't need to know. Each variable in Go exists as long as there are references to it. The storage location chosen by the implementation is irrelevant to the semantics of the language.
The storage location does have an effect on writing efficient programs. When possible, the Go compilers will allocate variables that are local to a function in that function's stack frame. However, if the compiler cannot prove that the variable is not referenced after the function returns, then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. Also, if a local variable is very large, it might make more sense to store it on the heap rather than the stack.
In the current compilers, if a variable has its address taken, that variable is a candidate for allocation on the heap. However, a basic escape analysis recognizes some cases when such variables will not live past the return from the function and can reside on the stack.
意思:從正確的角度來(lái)看,您不需要知道染坯。Go中的每個(gè)變量都存在均芽,只要有對(duì)它的引用即可。實(shí)現(xiàn)選擇的存儲(chǔ)位置與語(yǔ)言的語(yǔ)義無(wú)關(guān)单鹿。
存儲(chǔ)位置確實(shí)會(huì)影響編寫高效的程序掀宋。如果可能,Go編譯器將為該函數(shù)的堆棧幀中的函數(shù)分配本地變量仲锄。但是劲妙,如果編譯器在函數(shù)返回后無(wú)法證明變量未被引用,則編譯器必須在垃圾收集堆上分配變量以避免懸空指針錯(cuò)誤儒喊。此外镣奋,如果局部變量非常大,將它存儲(chǔ)在堆而不是堆棧上可能更有意義怀愧。
在當(dāng)前的編譯器中唆途,如果變量具有其地址,則該變量是堆上分配的候選變量掸驱。但是肛搬,基本的轉(zhuǎn)義分析可以識(shí)別某些情況,這些變量不會(huì)超過(guò)函數(shù)的返回值并且可以駐留在堆棧上毕贼。
Go的編譯器會(huì)決定在哪(堆or棧)分配內(nèi)存温赔,保證程序的正確性。
下面通過(guò)反匯編查看具體內(nèi)存分配情況:
新建 main.go
package main
import "fmt"
func main() {
var a [1]int
c := a[:]
fmt.Println(c)
}
查看匯編代碼
go tool compile -S main.go
輸出:
[root@localhost example]# go tool compile -S main.go
"".main STEXT size=183 args=0x0 locals=0x60
0x0000 00000 (main.go:5) TEXT "".main(SB), $96-0
0x0000 00000 (main.go:5) MOVQ (TLS), CX
0x0009 00009 (main.go:5) CMPQ SP, 16(CX)
0x000d 00013 (main.go:5) JLS 173
0x0013 00019 (main.go:5) SUBQ $96, SP
0x0017 00023 (main.go:5) MOVQ BP, 88(SP)
0x001c 00028 (main.go:5) LEAQ 88(SP), BP
0x0021 00033 (main.go:5) FUNCDATA $0, gclocals·f6bd6b3389b872033d462029172c8612(SB)
0x0021 00033 (main.go:5) FUNCDATA $1, gclocals·3ea58e42e2dc6c51a9f33c0d03361a27(SB)
0x0021 00033 (main.go:5) FUNCDATA $3, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB)
0x0021 00033 (main.go:6) PCDATA $2, $1
0x0021 00033 (main.go:6) PCDATA $0, $0
0x0021 00033 (main.go:6) LEAQ type.[1]int(SB), AX
0x0028 00040 (main.go:6) PCDATA $2, $0
0x0028 00040 (main.go:6) MOVQ AX, (SP)
0x002c 00044 (main.go:6) CALL runtime.newobject(SB)
0x0031 00049 (main.go:6) PCDATA $2, $1
0x0031 00049 (main.go:6) MOVQ 8(SP), AX
0x0036 00054 (main.go:8) PCDATA $2, $0
0x0036 00054 (main.go:8) PCDATA $0, $1
0x0036 00054 (main.go:8) MOVQ AX, ""..autotmp_4+64(SP)
鬼癣。陶贼。啤贩。。拜秧。
注意到有調(diào)用newobject痹屹!其中main.go:6說(shuō)明變量a的內(nèi)存是在堆上分配的!
修改main.go
package main
func main() {
var a [1]int
c := a[:]
println(c)
}
再查看匯編代碼
[root@localhost example]# go tool compile -S main.go
\"".main STEXT size=102 args=0x0 locals=0x28
0x0000 00000 (main.go:3) TEXT "".main(SB), $40-0
0x0000 00000 (main.go:3) MOVQ (TLS), CX
0x0009 00009 (main.go:3) CMPQ SP, 16(CX)
0x000d 00013 (main.go:3) JLS 95
0x000f 00015 (main.go:3) SUBQ $40, SP
0x0013 00019 (main.go:3) MOVQ BP, 32(SP)
0x0018 00024 (main.go:3) LEAQ 32(SP), BP
0x001d 00029 (main.go:3) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x001d 00029 (main.go:3) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x001d 00029 (main.go:3) FUNCDATA $3, gclocals·9fb7f0986f647f17cb53dda1484e0f7a(SB)
0x001d 00029 (main.go:4) PCDATA $2, $0
0x001d 00029 (main.go:4) PCDATA $0, $0
0x001d 00029 (main.go:4) MOVQ $0, "".a+24(SP)
0x0026 00038 (main.go:6) CALL runtime.printlock(SB)
0x002b 00043 (main.go:6) PCDATA $2, $1
0x002b 00043 (main.go:6) LEAQ "".a+24(SP), AX
0x0030 00048 (main.go:6) PCDATA $2, $0
0x0030 00048 (main.go:6) MOVQ AX, (SP)
0x0034 00052 (main.go:6) MOVQ $1, 8(SP)
0x003d 00061 (main.go:6) MOVQ $1, 16(SP)
0x0046 00070 (main.go:6) CALL runtime.printslice(SB)
0x004b 00075 (main.go:6) CALL runtime.printnl(SB)
0x0050 00080 (main.go:6) CALL runtime.printunlock(SB)
沒(méi)有發(fā)現(xiàn)調(diào)用newobject,這段代碼a是在堆棧上分配的枉氮。
結(jié)論:
Go 編譯器自行決定變量分配在堆椫狙埽或堆上,以保證程序的正確性聊替。
參考資料:
https://www.ardanlabs.com/blog/2015/01/stack-traces-in-go.html