官方说法

从一个正确的解释点来说,不需要知道。 准确地说,你并不需要知道。Golang 中的变量只要被引用就一直会存活,存储在堆上还是栈上由内部实现决定而和具体的语法没有关系。

知道变量的存储位置确实和效率编程有关系。如果可能,Golang 编译器会将函数的局部变量分配到函数栈帧(stack frame)上。然而,如果编译器不能确保变量在函数 return 之后不再被引用,编译器就会将变量分配到堆上来避免指针错误。而且,如果一个局部变量非常大,那么它也应该被分配到堆上而不是栈上。

当前情况下,如果一个变量被取地址,那么它就有可能被分配到堆上。然而,还要对这些变量做逃逸分析,如果函数 return 之后,变量不再被引用,则将其分配到栈上。

例子1(来源)

func main() {
    var a [1]int
    c := a[:]
    fmt.Println(c)
}

通过命令查看其汇编代码: go tool compile -S demo1.go

"".main STEXT size=200 args=0x0 locals=0x60
        0x0000 00000 (demo1.go:5)       TEXT    "".main(SB), $96-0
        0x0000 00000 (demo1.go:5)       MOVQ    TLS, CX
        0x0009 00009 (demo1.go:5)       MOVQ    (CX)(TLS*2), CX
        0x0010 00016 (demo1.go:5)       CMPQ    SP, 16(CX)
        0x0014 00020 (demo1.go:5)       JLS     190
        0x001a 00026 (demo1.go:5)       SUBQ    $96, SP
        0x001e 00030 (demo1.go:5)       MOVQ    BP, 88(SP)
        0x0023 00035 (demo1.go:5)       LEAQ    88(SP), BP
        0x0028 00040 (demo1.go:5)       FUNCDATA        $0, gclocals·69c1753bd5f81501d95132d08af04464(SB)
        0x0028 00040 (demo1.go:5)       FUNCDATA        $1, gclocals·57cc5e9a024203768cbab1c731570886(SB)
        0x0028 00040 (demo1.go:5)       LEAQ    type.[1]int(SB), AX
        0x002f 00047 (demo1.go:6)       MOVQ    AX, (SP)
        0x0033 00051 (demo1.go:6)       PCDATA  $0, $0
        0x0033 00051 (demo1.go:6)       CALL    runtime.newobject(SB)
        0x0038 00056 (demo1.go:6)       MOVQ    8(SP), AX
        0x003d 00061 (demo1.go:8)       MOVQ    AX, ""..autotmp_4+64(SP)
        0x0042 00066 (demo1.go:8)       MOVQ    $1, ""..autotmp_4+72(SP)
        0x004b 00075 (demo1.go:8)       MOVQ    $1, ""..autotmp_4+80(SP)
        0x0054 00084 (demo1.go:8)       MOVQ    $0, ""..autotmp_3+48(SP)
        0x005d 00093 (demo1.go:8)       MOVQ    $0, ""..autotmp_3+56(SP)
        0x0066 00102 (demo1.go:8)       LEAQ    type.[]int(SB), AX
        0x006d 00109 (demo1.go:8)       MOVQ    AX, (SP)
        0x0071 00113 (demo1.go:8)       LEAQ    ""..autotmp_4+64(SP), AX
        0x0076 00118 (demo1.go:8)       MOVQ    AX, 8(SP)
        0x007b 00123 (demo1.go:8)       PCDATA  $0, $1
        0x007b 00123 (demo1.go:8)       CALL    runtime.convT2Eslice(SB)
        0x0080 00128 (demo1.go:8)       MOVQ    24(SP), AX
        0x0085 00133 (demo1.go:8)       MOVQ    16(SP), CX
        0x008a 00138 (demo1.go:8)       MOVQ    CX, ""..autotmp_3+48(SP)
        0x008f 00143 (demo1.go:8)       MOVQ    AX, ""..autotmp_3+56(SP)
        0x0094 00148 (demo1.go:8)       LEAQ    ""..autotmp_3+48(SP), AX
        0x0099 00153 (demo1.go:8)       MOVQ    AX, (SP)
        0x009d 00157 (demo1.go:8)       MOVQ    $1, 8(SP)
        0x00a6 00166 (demo1.go:8)       MOVQ    $1, 16(SP)
        0x00af 00175 (demo1.go:8)       PCDATA  $0, $1
        0x00af 00175 (demo1.go:8)       CALL    fmt.Println(SB)
        0x00b4 00180 (demo1.go:9)       MOVQ    88(SP), BP
        0x00b9 00185 (demo1.go:9)       ADDQ    $96, SP
        0x00bd 00189 (demo1.go:9)       RET
        0x00be 00190 (demo1.go:9)       NOP
        0x00be 00190 (demo1.go:5)       PCDATA  $0, $-1
        0x00be 00190 (demo1.go:5)       CALL    runtime.morestack_noctxt(SB)
        0x00c3 00195 (demo1.go:5)       JMP     0

注意runtime.newobject!其中demo1.go:6说明变量a的内存是在堆上分配的

运行go tool compile -m demo1.go 参数-m是打印出编译优化

demo1.go:8:13: c escapes to heap
demo1.go:7:8: a escapes to heap
demo1.go:6:6: moved to heap: a
demo1.go:8:13: main ... argument does not escape

以上可以明显看出编译器的逃逸分析优化

func main() {
    var a [1]int
    c := a[:]
    println(c)
}

go tool compile -m demo1.go

demo1.go:3:6: can inline main
demo1.go:5:8: main a does not escape

可以看出a,c都在栈上分配 这是因为fmt.Println打印slice时会用到reflect.Type.Kind(),这是个接口方法,导致传入fmt.Println的参数被分配在堆上。

例2(来源知乎https://zhuanlan.zhihu.com/p/32686933?utm_source=qq&utm_medium=social)

一段会使内存不断增长的代码

type Node struct {
    next *Node
    payload [64]byte
}

func main() {
    curr := new(Node)
    for {
        curr.next = new(Node)
        curr = curr.next
    }
}

原因是第一个Node的new分配在栈上了,go编译器会对代码做逃逸分析,如果在函数中new分配一个小对象,而且这个小对象不会逃逸出去,则可以直接将其分配到栈上,所以上面代码在优化后相当于:

func main() {
    var n Node
    curr := &n
    for {
        curr.next = new(Node)
        curr = curr.next
    }
}

而当函数返回后调用GC不能及时时被回收

func f() {
    curr := new(Node)
    for i := 0; i < 10000000; i ++ {
        curr.next = new(Node)
        curr = curr.next
    }
}

func main() {
    f()
    runtime.GC()
    time.Sleep(time.Second * 100) //在这里查看进程内存使用
}

也就是说,函数返回后,栈上的数据还是会作为不可达来处理的,所以正常业务开发的时候,这个问题看起来也不算严重(从main到main_loop这一段的临时对象容易出事,注意一下即可)

解决办法是用二级指针

//用二级指针,new(*Node)会被逃逸优化,但new(Node)就不会了
func main() {
    curr := new(*Node)
    *curr = new(Node)
    for {
        (*curr).next = new(Node)
        *curr = (*curr).next
    }
}

//用return强制逃逸,避免优化
func f() *Node {
    curr := new(Node)
    for {
        curr.next = new(Node)
        curr = curr.next
    }
    return curr
}

func main() {
    f()
}

自己实践例2

用go trace 工具捕捉trace文件进行分析

func main() {
    f, _ := os.Create("trace.out")
    defer f.Close()
    trace.Start(f)
    defer trace.Stop()
    curr := new(Node)
    n := 0
    for {
        curr.next = new(Node)
        curr = curr.next
        n++
        if n == 10000000 {
            break
        }
        if n%1000 == 0 {
            runtime.GC()
        }
    }

}

当n是1000的倍数时,显示调用GC,效果没什么用

func main() {
    f, _ := os.Create("trace.out")
    defer f.Close()
    trace.Start(f)
    defer trace.Stop()
    curr := new(Node)
    n := 0
    for {
        curr.next = new(Node)
        curr = curr.next
        n++
        if n == 10000000 {
            break
        }
        time.Sleep(time.Nanosecond)
    }

}

当让休眠1纳秒却可以看出GC效果

func main() {
    f, _ := os.Create("trace.out")
    defer f.Close()
    trace.Start(f)
    defer trace.Stop()
    curr := new(Node)
    n := 0
    for {
        curr.next = new(Node)
        curr = curr.next
        n++
        if n == 10000000 {
            break
        }
        if n == 10000 || n == 1000000 || n == 2000000 {
            runtime.GC()
            //time.Sleep(time.Millisecond)
        }
    }

}

当两次GC调用之间存在较大内存扩增时,也可以观察出明显的GC效果

看来还是对内存管理和GC机制不熟

results matching ""

    No results matching ""