Go 堆栈的理解

在讲Go的堆栈之前,先温习一下堆栈基础知识。

什么是堆栈?在计算机中堆栈的概念分为:数据结构的堆栈和内存分配中堆栈。

数据结构的堆栈:

堆:堆可以被看成是一棵树,如:堆排序。在队列中,调度程序反复提取队列中第一个作业并运行,因为实际情况中某些时间较短的任务将等待很长时间才能结束,或者某些不短小,但具有重要性的作业,同样应当具有优先权。堆即为解决此类问题设计的一种数据结构。

栈:一种先进后出的数据结构。

这里着重讲的是内存分配中的堆和栈。

内存分配中的堆和栈

栈(操作系统):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。

堆(操作系统): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。

堆栈缓存方式

栈使用的是一级缓存, 他们通常都是被调用时处于存储空间中,调用完毕立即释放。

堆则是存放在二级缓存中,生命周期由虚拟机的垃圾回收算法来决定(并不是一旦成为孤儿对象就能被回收)。所以调用这些对象的速度要相对来得低一些。

堆栈跟踪

下面讨论堆栈跟踪信息以及如何在堆栈中识别函数所传递的参数。

以下测试案例的版本是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是一个简单的程序, main函数在第5行调用Example函数。Example函数在第9行声明,它有三个参数,一个字符串slice,一个字符串和一个整数。它的方法体也很简单,只有一行,debug.PrintStack(),这会立即产生一个堆栈跟踪信息:

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

堆栈跟踪信息:

第一行显示运行的goroutine是id为 1的goroutine。

第二行 debug.Stack()被调用

第四行 debug.PrintStack() 被调用

第六行 调用debug.PrintStack()的代码位置,位于main package下的Example函数。它也显示了代码所在的文件和路径,以及debug.PrintStack()发生的行数(第10行)。

第八行 也调用Example的函数的名字,它是main package的main函数。它也显示了文件名和路径,以及调用Example函数的行数。

下面主要分析 传递Example函数传参信息

// 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函数的声明,调用以及传递给它的值的信息。当你比较函数的声明以及传递的值时,发现它们并不一致。函数声明只接收三个参数,而堆栈中却显示6个16进制表示的值。理解这一点的关键是要知道每个参数类型的实现机制。

让我们看第一个[]string类型的参数。slice是引用类型,这意味着那个值是一个指针的头信息(header value),它指向一个字符串。对于slice,它的头是三个word数,指向一个数组。因此前三个值代表这个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代表第一个参数[]string的指针,0x2代表slice长度,0x4代表容量。这三个值代表第一个参数。

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个和第5个参数代表字符串的参数。0x4abd9e是指向这个字符串底层数组的指针,0x5是"hello"字符串的长度,他们俩作为第二个参数。

9.4.1.png

第三个参数是一个整数,它是一个简单的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) 

显示堆栈中的最后一个参数就是Example声明中的第三个参数,它的值是0xa,也就是整数10。

9.4.2.png

Methods

接下来让我们稍微改动一下程序,让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行新增加了一个类型trace,在第15将example改变为trace的pointer receiver的一个方法。第12行声明t的类型为trace,第13行调用它的方法。

因为这个方法声明为pointer receiver的方法,Go使用t的指针来支持receiver type,即使代码中使用值来调用这个方法。当程序运行时,堆栈跟踪信息如下:

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。方法名和报包名中间有(*trace)。第二个值得注意的是堆栈信息中方法的第一个参数为receiver的值。方法调用总是转换成函数调用,并将receiver的值作为函数的第一个参数。我们可以总堆栈信息中看到实现的细节。

Packing

import (
   "runtime/debug"
)

func main() {
   Example(true, false, true, 25)
}
func Example(b1, b2, b3 bool, i uint8) {

   debug.PrintStack()
}

再次改变Example的方法,让它接收4个参数。前三个参数是布尔类型的,第四个参数是8bit无符号整数。布尔类型也是8bit表示的,所以这四个参数可以被打包成一个word,包括32位架构和64位架构。当程序运行的时候,会产生有趣的堆栈:

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

可以看到四个值被打包成一个单一的值了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)

显示了堆栈的值如何和参数进行匹配的。true用1表示,占8bit, false用0表示,占8bit,uint8值25的16进制为x19,用8bit表示。我们课哟看到它们是如何表示成一个word值的。

Go运行时提供了详细的信息来帮助我们调试程序。通过堆栈跟踪信息stack trace,解码传递个堆栈中的方法的参数有助于我们快速定位BUG。

变量是堆(heap)还是堆栈(stack)

写过c语言都知道,有明确的堆栈和堆的相关概念。而Go声明语法并没有提到堆栈或堆,只是在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.

意思:从正确的角度来看,您不需要知道。Go中的每个变量都存在,只要有对它的引用即可。实现选择的存储位置与语言的语义无关。

存储位置确实会影响编写高效的程序。如果可能,Go编译器将为该函数的堆栈帧中的函数分配本地变量。但是,如果编译器在函数返回后无法证明变量未被引用,则编译器必须在垃圾收集堆上分配变量以避免悬空指针错误。此外,如果局部变量非常大,将它存储在堆而不是堆栈上可能更有意义。

在当前的编译器中,如果变量具有其地址,则该变量是堆上分配的候选变量。但是,基本的转义分析可以识别某些情况,这些变量不会超过函数的返回值并且可以驻留在堆栈上。

Go的编译器会决定在哪(堆or栈)分配内存,保证程序的正确性。

下面通过反汇编查看具体内存分配情况:

新建 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)
。。。。。

注意到有调用newobject!其中main.go:6说明变量a的内存是在堆上分配的!

修改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)

没有发现调用newobject,这段代码a是在堆栈上分配的。

结论:

Go 编译器自行决定变量分配在堆栈或堆上,以保证程序的正确性。

参考资料:

https://www.ardanlabs.com/blog/2015/01/stack-traces-in-go.html

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

https://golang.org/doc/faq

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,122评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,070评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,491评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,636评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,676评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,541评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,292评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,211评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,655评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,846评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,965评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,684评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,295评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,894评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,012评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,126评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,914评论 2 355

推荐阅读更多精彩内容