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

3 回复

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仍然指向这个地址

内存泄漏的根本原因

  1. 悬挂指针:当 doSomeThingsToTheRequest 函数返回时,localInfo 的内存被释放(如果是栈分配)或等待GC回收(如果逃逸到堆),但 req.newInfo 仍然持有这个地址的指针。

  2. 内存分析显示: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 查看内存使用情况。

关键点总结

  1. 这不是GC怪异现象:GC工作正常,问题在于代码逻辑错误
  2. 悬挂指针:指针指向了已释放的内存区域
  3. pprof显示正确:inuse_space显示内存仍在使用,因为指针引用仍然存在
  4. 解决方案:确保指针始终指向有效的堆分配内存

这个问题在Go中很常见,特别是在需要修改或复制结构体内部指针字段时。正确的内存管理是确保所有指针都指向有效的、适当生命周期的内存区域。

回到顶部