问题场景
分析一下,下面代码的输出是什么(判断a==c)的部分
package main
import (
"fmt"
"runtime"
)
type obj struct{}
func main() {
a := &obj{}
fmt.Printf("%p\n", a)
c := &obj{}
fmt.Printf("%p\n", c)
fmt.Println(a == c)
}
很多人可能一看,a和c完全是2个不同的对象实例,便认为a和c具备不同的内存地址,故而判断a==c的结果为false。我也是一样。我们看一下实际输出:
0x1181f88
0x1181f88
true
问题分析
要分析上面的问题,就需要了解一些Golang内存分配,以及变量在内存逃逸的知识。上面的代码,有打印a和c的内存地址。倘若我们去掉任意一个(或者将打印内存的地址都去掉也一样),则 a==c 的判断输出,就是 false。再看一下代码:
package main
import (
"fmt"
)
type obj struct{}
func main() {
a := &obj{}
//fmt.Printf("%p\n", a)
c := &obj{}
fmt.Printf("%p\n", c)
fmt.Println(a == c)
}
输出:
0x1181f88
false
那么,可以看出,是 fmt.Printf 影响了最终结果的判断。好吧,我们看一下,上面代码的内存逃逸情况分析:
go run -gcflags '-m -l' main.go
# command-line-arguments
./main.go:13:16: c escapes to heap
./main.go:12:10: &obj literal escapes to heap
./main.go:14:19: a == c escapes to heap
./main.go:10:10: main &obj literal does not escape
./main.go:13:15: main ... argument does not escape
./main.go:14:16: main ... argument does not escape
0x1181f88
false
可以看到,变量c从栈内存,逃逸到了堆内存上。而变量a没有逃逸(注意:上面代码中,有 fmt.Printf("%p\n", c),没有 fmt.Printf("%p\n", a) )。由此可以简单判断,是 fmt.Printf 导致变量产生了内存由栈向堆的逃逸。
回到最开始的问题上。
如果代码中,即打印 a,也打印b 的变量内存地址。则会导致 a 和 c,都逃逸到堆内存上。所以,我们的问题就来了。
- 为什么 fmt.Printf 会导致变量的内存逃逸?
- 为什么逃逸到了堆内存,2个变量就一样了?
问题1:为什么 fmt.Printf 会导致变量的内存逃逸?
其实,fmt.Printf 第二个参数,是一个 interface 类型。而 fmt.Printf 的内部实现,使用了反射 reflect,正是由于 reflect 才导致变量从栈向堆内存的逃逸成为可能(注意,并非所有reflect操作都会导致内存逃逸,具体还得看怎么使用reflect的)。我们简单总结为:
使用 fmt.Printf 由于其函数第二个参数是接口类型,而函数内部最终实现使用了 reflect 机制,导致变量从栈逃逸到堆内存。
问题2:为什么变量 a 和 c 逃逸到堆内存后,内存地址就一样了?
这是因为,堆上内存分配调用了 runtime 包的 newobject 函数。而 newobject 函数其实本质上会调用 runtime 包内的 mallocgc 函数。这个函数有点特别:
// Allocate an object of size bytes.
// Small objects are allocated from the per-P cache's free lists.
// Large objects (> 32 kB) are allocated straight from the heap.
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {
if gcphase == _GCmarktermination {
throw("mallocgc called with gcphase == _GCmarktermination")
}
// 关键部分,如果要分配内存的变量不占用实际内存,则直接用 golang 的全局变量 zerobase 的地址。
if size == 0 {
return unsafe.Pointer(&zerobase)
}
// ...
}
函数比较长,我做了截取。这函数内有一个判断。 如果要分配内存的变量不占用实际内存,则直接用 golang 的全局变量 zerobase 的地址。而我们的变量 a 和 变量 c 有一个共同特点,就是它们是“空 struct”,空 struct 是不占用内存空间的。
所以,a 和 c 是空 struct,再做内存分配的时候,使用了 golang 内部全局私有变量 zerobase 的内存地址。
如何验证 a 和 c 都使用的是 runtime包内的 zerobase 内存地址?
改一下 runtime 包中,mallocgc 函数所在的文件 runtime/malloc.go 增加一个函数 GetZeroBasePtr ,这个函数,专门用于返回 zerobase 的地址,如下:
// base address for all 0-byte allocations
var zerobase uintptr
func GetZeroBasePtr() unsafe.Pointer {
return unsafe.Pointer(&zerobase)
}
好了,我们回过头再改一下测试代码:
package main
import (
"fmt"
"runtime"
)
type obj struct{}
func main() {
a := &obj{}
// 打印 a 的地址
fmt.Printf("%p\n", a)
c := &obj{}
// 打印 c 的地址
fmt.Printf("%p\n", c)
fmt.Println(a == c)
// 打印 runtime 包内的 zerobase 的地址
ptr := runtime.GetZeroBasePtr()
fmt.Printf("golang inner zerobase ptr: %p\n", ptr)
}
重新编译:
// 注意,改了 golang 的源码,再编译的话,必须加 -a 参数
go build -a
结果输出如下:
0x1181f88
0x1181f88
true
golang inner zerobase ptr: 0x1181f88
问题得证。
参考:
- https://studygolang.com/topics/8655\#reply0
- https://golang.org/src/runtime/malloc.go
- https://studygolang.com/articles/5790
- http://legendtkl.com/2017/04/02/golang-alloc/
- http://reusee.github.io/post/escape_analysis/
欢迎关注“海角之南”公众号获取更新动态
有疑问加站长微信联系(非本文作者)