Golang中内存泄漏还是GC的怪异现象?
Golang中内存泄漏还是GC的怪异现象? 你好,
我正在研究 runtime/pprof 内存分析工具,并认为我发现了一个内存泄漏。经过一些调查后,我有点困惑,开始不确定这到底是不是一个内存泄漏。(或者是否与垃圾回收器相关的某些潜在奇怪现象有关)
考虑以下示例。(它没有复现错误,但与我们自己的生产代码非常相似。) https://play.golang.org/p/CgwjgZwm25N
当我使用 go tool pprof 进行调查时,它显示在 “inuse_space” 样本中仍然分配了一些数据。
func (req *Request) Init(operation string, oldConfigData string, newConfigData string) *Request {
req.operation = operation
req.oldInfo = NewInfo(oldConfigData) // 注意,这里没有内存被使用...?
req.newInfo = NewInfo(newConfigData) // 这里我在 "inuse_space" 中仍然有内存
return req
}
我确认了 functionalCall() 函数确实返回了。所以这意味着(至少根据我的理解)req 变量应该超出作用域并被垃圾回收器清理。但根据内存分析,情况并非如此。
我由此得出的结论是:这意味着 req.newInfo 指针变量在后续的 doSomeThingsToTheRequest() 函数中被使用/存储在了某个地方。
然后,我想通过修改 doSomeThingsToTheRequest() 函数,使其在后续任何操作中使用一个本地副本来验证这一点。我期望内存分析工具会显示现在 localInfo 是泄漏内存的变量。但事实并非如此,它仍然显示原始变量持有内存。(如上所述…)——> 我感到困惑,我认为是内存泄漏的地方并没有转移。
func doSomeThingsToTheRequest(req *Request){
var localInfo Info
localInfo = *req.newInfo
req.newInfo = &localInfo
fmt.Println(&localReq)
}
现在,我有点困惑这到底是不是内存泄漏。
更多关于Golang中内存泄漏还是GC的怪异现象?的实战教程也可以访问 https://www.itying.com/category-94-b0.html
Anon5710:
req.oldInfo = NewInfo(oldConfigData) <— 注意这里没有使用内存…?
你所说的“这里没有使用内存”是什么意思?NewInfo 返回一个新分配的 *Info 结构体指针。
更多关于Golang中内存泄漏还是GC的怪异现象?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
是的,那里有一个新分配的 *Info 结构体指针。但这个变量在我的函数结束后已被垃圾回收器清理,而 newInfo 那个却没有。
func main() {
fmt.Println("hello world")
}
根据你提供的代码片段和分析,这确实是一个典型的内存泄漏场景,而不是GC的怪异现象。让我通过代码示例来解释原因。
问题分析
在你的代码中,req.newInfo 被重新指向了一个局部变量的地址,这导致了悬挂指针问题:
func doSomeThingsToTheRequest(req *Request){
var localInfo Info // 局部变量在栈上分配
localInfo = *req.newInfo
req.newInfo = &localInfo // 问题在这里:将指针指向局部变量
fmt.Println(&localInfo)
} // 函数返回时,localInfo超出作用域,但req.newInfo仍然指向这个地址
内存泄漏的根本原因
-
悬挂指针:当
doSomeThingsToTheRequest函数返回时,localInfo的内存被释放(如果是栈分配)或等待GC回收(如果逃逸到堆),但req.newInfo仍然持有这个地址的指针。 -
内存分析显示:pprof的inuse_space显示内存仍然被占用,因为指针仍然指向某块内存,GC无法回收这些内存。
验证示例
这里是一个完整的示例来演示这个问题:
package main
import (
"fmt"
"runtime"
"time"
)
type Info struct {
data string
}
type Request struct {
operation string
oldInfo *Info
newInfo *Info
}
func NewInfo(data string) *Info {
return &Info{data: data}
}
func (req *Request) Init(operation, oldData, newData string) *Request {
req.operation = operation
req.oldInfo = NewInfo(oldData)
req.newInfo = NewInfo(newData)
return req
}
func doSomeThingsToTheRequest(req *Request) {
var localInfo Info
localInfo = *req.newInfo
req.newInfo = &localInfo // 内存泄漏点
fmt.Printf("localInfo address: %p\n", &localInfo)
}
func main() {
// 模拟生产环境
var requests []*Request
for i := 0; i < 1000; i++ {
req := &Request{}
req.Init("operation", "old", "new")
doSomeThingsToTheRequest(req)
// 保留引用,模拟实际使用
requests = append(requests, req)
// 每100次打印内存状态
if i%100 == 0 {
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("Iteration %d: Alloc = %v MiB\n", i, m.Alloc/1024/1024)
}
time.Sleep(10 * time.Millisecond)
}
// 强制GC
runtime.GC()
time.Sleep(1 * time.Second)
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("\nFinal: Alloc = %v MiB\n", m.Alloc/1024/1024)
fmt.Printf("HeapObjects = %v\n", m.HeapObjects)
}
解决方案
正确的做法应该是创建新的堆分配对象:
func doSomeThingsToTheRequest(req *Request) {
// 正确做法:创建新的堆分配对象
localInfo := &Info{
data: req.newInfo.data,
}
// 或者使用深拷贝
// localInfo := &Info{}
// *localInfo = *req.newInfo
req.newInfo = localInfo // 现在指向有效的堆内存
}
// 或者如果Info结构体有方法,可以添加Clone方法
func (i *Info) Clone() *Info {
return &Info{
data: i.data,
}
}
func doSomeThingsToTheRequestV2(req *Request) {
req.newInfo = req.newInfo.Clone() // 使用Clone方法
}
使用pprof验证修复
package main
import (
"net/http"
_ "net/http/pprof"
"time"
)
func main() {
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
// 运行你的业务逻辑
runBusinessLogic()
select {}
}
func runBusinessLogic() {
// 你的业务逻辑代码
for {
req := &Request{}
req.Init("op", "old", "new")
doSomeThingsToTheRequest(req) // 修复后的版本
time.Sleep(100 * time.Millisecond)
}
}
运行后可以通过 go tool pprof http://localhost:6060/debug/pprof/heap 查看内存使用情况。
关键点总结
- 这不是GC怪异现象:GC工作正常,问题在于代码逻辑错误
- 悬挂指针:指针指向了已释放的内存区域
- pprof显示正确:inuse_space显示内存仍在使用,因为指针引用仍然存在
- 解决方案:确保指针始终指向有效的堆分配内存
这个问题在Go中很常见,特别是在需要修改或复制结构体内部指针字段时。正确的内存管理是确保所有指针都指向有效的、适当生命周期的内存区域。

