Golang中变量为何会保留在堆上?
Golang中变量为何会保留在堆上? 大家好
我觉得我之前的问题没有解释清楚,我会在这个新话题中尝试更好地说明。 之前的讨论可以在这里找到(这个话题可以关闭了)。
我正在使用 pprof 包进行内存性能分析,结果表明有一些变量存储在堆上,我无法解释,甚至可能是内存泄漏。
我使用以下代码写入内存性能分析文件,该代码通过 restApi 从机器人框架调用(因此它在自己的协程中运行)。
func onGetMemProfile(w http.ResponseWriter, r *http.Request) {
memProfile, err := os.Create("memProfile.pprof")
if err != nil {
fmt.Println("memProfile could not be created.")
}
runtime.GC() //运行垃圾回收
if err := pprof.WriteHeapProfile(memProfile); err != nil {
fmt.Println("could not write memory profile: ", err)
}
}
当我研究特定场景后两个内存性能分析文件之间的差异时,性能分析工具告诉我有一个字符串变量保留在堆上,这我无法解释。
这发生在以下代码片段中:
func (info *Info) Values(path string) (ret []gjson.Result, status QueryStatus) {
status = QueryUnknown
test := info.data + " "
result := gjson.Get(test, path)
if result.Exists() {
status = QuerySuccessful
ret = result.Array()
} else {
status = QueryConfigMissing
}
return
}
如果不将 info.data 复制到 test 变量中,性能分析工具表明 info.data 的内存会在更高层级被保留。通过这次复制,根据性能分析数据,info.data 没有被保留。
性能分析工具的输出如下(针对 inuse_space 样本):
Total: 7.56kB 7.56kB (flat, ■■■) 0.013%
258 . . }
259 . .
260 . . func (info *Info) Values(path string) (ret []gjson.Result, status QueryStatus) {
261 . . status = QueryUnknown
262 . .
263 7.56kB 7.56kB test := info.data + " "
264 . .
265 . . result := gjson.Get(test, path)
266 . . if result.Exists() {
267 . . status = QuerySuccessful
268 . . ret = result.Array()
请注意,在我们测试的场景中,这个函数被调用了很多次。查看性能分析的 alloc_object 和 inuse_object 样本空间时,只有 10 次(总共 1344 次)字符串被保留在堆上。
alloc_object sample : 263 1344 1344 test := info.data + " "
inuse_object sampe : 263 10 10 test := info.data + " "
我已经检查了 gjson 中与 get 相关的突出错误,但没有发现任何问题。 我已在调试器中检查代码,以验证 result 不包含任何指向 test 变量的指针。
问题:
- 我是否正确解读了性能分析结果?在我的场景结束后,test 变量是否仍然驻留在堆上?
- 写入堆性能分析文件的代码是否正确?我是否正确使用了它?在哪个 goroutine 上调用 WriteHeapProfile() 函数是否有影响?
- 为什么 test 变量会被保留在堆上?(我没有看到任何对它的引用)
- 这种行为是否表明存在内存泄漏?
- 为什么垃圾回收器没有在所有情况下清理 test 变量?什么可以解释为什么在 10/1344 的情况下变量没有被清理?
- 这是否表明 gjson 库中存在错误?
提前感谢任何帮助。
更多关于Golang中变量为何会保留在堆上?的实战教程也可以访问 https://www.itying.com/category-94-b0.html
你尝试过
go build -gcflags="-m"
来查看逃逸分析吗?
更多关于Golang中变量为何会保留在堆上?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
我觉得我之前的问题没有解释清楚,我会在这个新话题中尝试说得更好一些。
你标题中的问题很明确:为什么这个变量被保留在堆上?你在这个问题以及之前问题中的文字所反映出的问题,表明了你对Go垃圾收集器和工具的(错误?)假设。
你需要理解垃圾收集的一般原理,特别是Go语言中的工作原理。你还需要理解Go工具的工作方式。
首先要回答的问题是,你使用的go version是什么。Go语言在不断优化并持续改进。例如,即将发布的版本引入了一种新的、改进的内存管理技术。
下一个要回答的问题是使用哪个Go工具。pprof使用采样来识别程序中的热点。对于栈和堆的分配,可以从Go编译器开始。关于分配的基础介绍(2019年),请观看这个视频:
你得到了什么结果?你能解释它们吗?
petrus:
首先要回答的问题是,你的
go version是什么?
我被迫使用一个相当旧的版本:
go version go1.10.1 linux/amd64
感谢你提供的链接,它证实了我的猜测。
vorsprung:
你试过
go build -gcflags="-m"来查看逃逸分析吗?
我现在试了,我花了一些时间才弄清楚如何对所有文件(而不仅仅是 main.go)执行此操作。
我尝试了:
go build -gcflags "-m=2 -l" directory_name
go build -gcflags "-m=2 -l" ./...
逃逸分析明确指出我创建的字符串逃逸到了堆上。
info.data + " " escapes to heap
我仍然不确定原因。数据没有向上共享,只向下传递。petrus 提供的链接表明,大小在这里可能是一个因素。(数据大于 2KB)但我以为栈会增长以适应这种情况。
下图显示了每次调用 Values() 函数时存储在堆上的对象(info.data + " ")数量。此信息是通过获取 @init 和每次调用 Values() 函数之后的内存配置文件差异得到的。
我原本以为每次调用 runtime.GC()(在 X 轴的每个单位处调用)都会清理堆上所有未使用的数据。图表告诉我这根本不是真的。它显示一些对象在稍后的时间点才被清理。
我可能需要查看一下我使用的 Go 版本的垃圾收集器工作原理。

所以,我在原帖中的问题仍然存在。
我所看到的行为是 内存泄漏 还是 垃圾收集器内部工作机制 的影响?
此致
根据你的性能分析数据和代码,我来解释一下Go语言中变量逃逸到堆上的原因以及你的具体问题。
变量逃逸分析
在Go中,变量是否分配在堆上由编译器的逃逸分析决定。当变量的生命周期超出函数作用域时,它就会"逃逸"到堆上。
代码分析
func (info *Info) Values(path string) (ret []gjson.Result, status QueryStatus) {
status = QueryUnknown
test := info.data + " " // 这里可能发生逃逸
result := gjson.Get(test, path)
// ...
}
问题解答
1. 性能分析结果解读
是的,你正确解读了性能分析结果。test变量确实逃逸到了堆上。inuse_object显示10个对象仍在堆上,而alloc_object显示总共分配了1344次。
2. 性能分析代码正确性
你的性能分析代码基本正确:
func onGetMemProfile(w http.ResponseWriter, r *http.Request) {
memProfile, err := os.Create("memProfile.pprof")
if err != nil {
fmt.Println("memProfile could not be created.")
return // 添加return避免空指针
}
defer memProfile.Close() // 重要:确保文件关闭
runtime.GC() // 运行垃圾回收
if err := pprof.WriteHeapProfile(memProfile); err != nil {
fmt.Println("could not write memory profile: ", err)
}
}
在哪个goroutine调用WriteHeapProfile()没有影响,它会收集整个进程的堆信息。
3. 为什么test变量被保留在堆上
test变量逃逸的原因可能是:
// gjson.Get内部可能使test逃逸
result := gjson.Get(test, path)
gjson.Get函数签名是:
func Get(json, path string) Result
虽然参数是值传递,但Go编译器可能认为:
test被传递给外部包函数- 外部函数可能持有对字符串的引用
- 因此
test需要分配在堆上
4. 这是否是内存泄漏
不一定。10/1344的对象仍在堆上可能是:
- 垃圾回收尚未运行
- 这些对象被其他未显示的引用持有
- 性能分析时的快照效应
5. 为什么GC没有清理所有test变量
GC没有立即清理的原因:
// 可能的解释:
// 1. 字符串池化:Go的字符串可能被intern
// 2. 临时对象在GC周期之间存活
// 3. 逃逸分析保守性:编译器可能过度保守
// 验证方法:查看字符串内容是否相同
fmt.Printf("info.data length: %d\n", len(info.data))
6. gjson库是否有bug
不一定。可以检查gjson是否无意中保留了引用:
// 测试:直接传递字符串字面量
func TestNoEscape() {
// 这应该不会逃逸
result := gjson.Get(`{"test": "value"}`, "test")
_ = result
}
// 对比测试:使用变量
func TestWithEscape() {
data := `{"test": "value"}`
result := gjson.Get(data, "test") // data可能逃逸
_ = result
}
深入分析建议
使用逃逸分析标志编译:
go build -gcflags="-m -m" your_package
这会显示详细的逃逸分析信息,例如:
./main.go:263:6: test escapes to heap:
./main.go:263:6: flow: {storage for ... argument} = &test:
./main.go:263:6: from test (spill) at ./main.go:263:6
./main.go:263:6: from ... argument (slice-literal-element) at ./main.go:265:20
优化建议
如果确实需要避免逃逸,可以考虑:
func (info *Info) Values(path string) (ret []gjson.Result, status QueryStatus) {
status = QueryUnknown
// 方案1:使用字符串字面量(如果info.data是常量)
// result := gjson.Get(info.data, path)
// 方案2:避免字符串拼接
result := gjson.Get(info.data, path)
if result.Exists() {
status = QuerySuccessful
ret = result.Array()
} else {
status = QueryConfigMissing
}
return
}
关键结论
- 10个对象在堆上不一定是泄漏,可能是正常的GC行为
- 字符串拼接
info.data + " "创建了新字符串,可能触发逃逸 - 传递给外部包函数的变量更容易逃逸
- 使用
-gcflags="-m"编译可以确认逃逸原因
性能分析显示的是特定时刻的堆状态,不代表永久性内存泄漏。GC会在适当的时候回收这些对象。

