Go 堆棧的理解

在講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ù)栓辜。

9.4.png

// 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ù)。

9.4.1.png

第三個(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贩毕。

9.4.2.png

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

https://zhuanlan.zhihu.com/p/28484133

https://golang.org/doc/faq

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末楼肪,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子惹悄,更是在濱河造成了極大的恐慌春叫,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,539評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件泣港,死亡現(xiàn)場(chǎng)離奇詭異暂殖,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)当纱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評(píng)論 3 396
  • 文/潘曉璐 我一進(jìn)店門央星,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人惫东,你說(shuō)我怎么就攤上這事莉给。” “怎么了廉沮?”我有些...
    開封第一講書人閱讀 165,871評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵颓遏,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我滞时,道長(zhǎng)叁幢,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,963評(píng)論 1 295
  • 正文 為了忘掉前任坪稽,我火速辦了婚禮曼玩,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘窒百。我一直安慰自己黍判,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評(píng)論 6 393
  • 文/花漫 我一把揭開白布篙梢。 她就那樣靜靜地躺著顷帖,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上贬墩,一...
    開封第一講書人閱讀 51,763評(píng)論 1 307
  • 那天榴嗅,我揣著相機(jī)與錄音,去河邊找鬼陶舞。 笑死嗽测,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的肿孵。 我是一名探鬼主播唠粥,決...
    沈念sama閱讀 40,468評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼颁井!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起蠢护,我...
    開封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤雅宾,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后葵硕,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體眉抬,經(jīng)...
    沈念sama閱讀 45,850評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評(píng)論 3 338
  • 正文 我和宋清朗相戀三年懈凹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蜀变。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,144評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡介评,死狀恐怖库北,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情们陆,我是刑警寧澤寒瓦,帶...
    沈念sama閱讀 35,823評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站坪仇,受9級(jí)特大地震影響杂腰,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜椅文,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評(píng)論 3 331
  • 文/蒙蒙 一喂很、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧皆刺,春花似錦少辣、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春煎殷,著一層夾襖步出監(jiān)牢的瞬間屯伞,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工豪直, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留劣摇,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,415評(píng)論 3 373
  • 正文 我出身青樓弓乙,卻偏偏與公主長(zhǎng)得像末融,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子暇韧,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容