问题场景
分析一下,下面代码的输出是什么(判断a==c)的部分
1 | package main |
很多人可能一看,a和c完全是2个不同的对象实例,便认为a和c具备不同的内存地址,故而判断a==c的结果为false。我也是一样。我们看一下实际输出:
1 | 0x1181f88 |
问题分析
要分析上面的问题,就需要了解一些Golang内存分配,以及变量在内存逃逸的知识。上面的代码,有打印a和c的内存地址。倘若我们去掉任意一个(或者将打印内存的地址都去掉也一样),则 a==c 的判断输出,就是 false。再看一下代码:
1 | package main |
输出:
1 | 0x1181f88 |
那么,可以看出,是 fmt.Printf 影响了最终结果的判断。好吧,我们看一下,上面代码的内存逃逸情况分析:
1 | go run -gcflags '-m -l' main.go |
1 |
|
可以看到,变量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 函数。这个函数有点特别:
1 | // Allocate an object of size bytes. |
函数比较长,我做了截取。这函数内有一个判断。 如果要分配内存的变量不占用实际内存,则直接用 golang 的全局变量 zerobase 的地址。而我们的变量 a 和 变量 c 有一个共同特点,就是它们是“空 struct”,空 struct 是不占用内存空间的。
所以,a 和 c 是空 struct,再做内存分配的时候,使用了 golang 内部全局私有变量 zerobase 的内存地址。
如何验证 a 和 c 都使用的是 runtime包内的 zerobase 内存地址?
改一下 runtime 包中,mallocgc 函数所在的文件 runtime/malloc.go 增加一个函数 GetZeroBasePtr ,这个函数,专门用于返回 zerobase 的地址,如下:
1 | // base address for all 0-byte allocations |
好了,我们回过头再改一下测试代码:
1 | package main |
重新编译:
1 | // 注意,改了 golang 的源码,再编译的话,必须加 -a 参数 |
结果输出如下:
1 | 0x1181f88 |
问题得证。