Golang中为什么使用GODEBUG=madvdontneed=1和FreeOSMemory后内存长时间不被操作系统回收?

Golang中为什么使用GODEBUG=madvdontneed=1和FreeOSMemory后内存长时间不被操作系统回收? 没有 goroutine 泄漏。 在某些频繁进行内存分配和释放的代码路径中,即使我停止测试很长时间,操作系统也没有回收内存。 Go版本:go1.20.3

curl 127.0.0.1:10080/debug/pprof/heap?debug=1 | less

截屏2023-07-14 上午10.45.25

在我设置 runtime.MemProfileRate = 1 之后

curl 127.0.0.1:10080/debug/pprof/heap?debug=1 | less
heap profile: 982711: 384712832 [306603837: 49470010600] @ heap/2
…
# runtime.MemStats
# Alloc = 469250056
# TotalAlloc = 49938819968
# Sys = 3191753112
# Lookups = 0
# Mallocs = 347135802
# Frees = 345386293
# HeapAlloc = 469250056
# HeapSys = 2800910336
# HeapIdle = 2318524416
# HeapInuse = 482385920
# HeapReleased = 1903173632
# HeapObjects = 1749509
# Stack = 5079040 / 5079040
# MSpan = 3365920 / 16238400
# MCache = 48000 / 62400
# BuckHashSys = 23255276
# GCSys = 64164064
# OtherSys = 282043596
# NextGC = 782366512
# LastGC = 1689328547695610282
  1. 为什么 HeapInuse(482385920) 不等于 384712832 并且 HeapObjects(1749509) 不等于 982711?
  2. 我如何知道是谁引用了这些内存?

看起来在 Golang 中定位内存泄漏比在 C++ 中更困难…


更多关于Golang中为什么使用GODEBUG=madvdontneed=1和FreeOSMemory后内存长时间不被操作系统回收?的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

是否有办法忽略Go运行时的行为并强制释放内存,以便我能够确定是否存在内存泄漏?

更多关于Golang中为什么使用GODEBUG=madvdontneed=1和FreeOSMemory后内存长时间不被操作系统回收?的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


Golang 中的 GODEBUG 环境变量提供了多种调试选项,包括 madvdontneed=1 标志。当设置此标志时,它会建议操作系统不要回收 Go 运行时最近释放的内存页。此标志的目的是通过减少向操作系统释放内存的开销来提高性能。

然而,设置 madvdontneed=1 并使用 FreeOSMemory 确实可能导致内存被 Go 运行时持有更长时间。当您使用 runtime.FreeOSMemory() 显式释放内存时,Go 运行时可能仍会保留一些内存以供未来分配,以避免频繁进行内存分配和释放的系统调用。

Go 运行时管理其自身的内存分配和垃圾回收,并且它有自己的启发式方法来决定何时向操作系统释放内存。默认情况下,Go 运行时尝试在为未来分配保留内存与将内存释放回操作系统之间取得平衡。

如果您发现操作系统长时间未回收内存,这很可能是由于 Go 运行时及其内存管理算法的行为所致。此行为可能因 Go 版本、操作系统和其他因素而异。

如果您面临特定的内存相关问题,例如内存使用过多或内存泄漏,建议分析和优化您的代码,并确保高效使用资源。Go 性能分析器(go tool pprof)有助于识别内存使用模式并优化代码中内存密集的部分。

总的来说,在使用像 madvdontneed=1FreeOSMemory 这样的高级内存管理选项时,需要格外注意。它们提供了控制和性能改进,但也可能产生副作用,需要在您的具体使用场景中仔细考虑。

在Go 1.20.3中,GODEBUG=madvdontneed=1FreeOSMemory后内存不被操作系统回收是正常行为。这涉及到Go内存管理器的设计决策。

问题分析

1. HeapInuse与HeapObjects不匹配的原因

HeapInuse表示正在使用的堆内存,而heap profile中的384712832字节是采样统计的结果。由于runtime.MemProfileRate = 1,每个分配都会被采样,但profile显示的是活跃对象的统计,不包括已被垃圾回收的对象。

package main

import (
    "fmt"
    "runtime"
    "runtime/debug"
    "time"
)

func main() {
    runtime.MemProfileRate = 1
    
    // 分配一些内存
    var slices [][]byte
    for i := 0; i < 1000; i++ {
        slices = append(slices, make([]byte, 1024*1024))
    }
    
    // 释放部分内存
    slices = slices[:500]
    
    runtime.GC()
    debug.FreeOSMemory()
    
    var stats runtime.MemStats
    runtime.ReadMemStats(&stats)
    
    fmt.Printf("HeapInuse: %d\n", stats.HeapInuse)
    fmt.Printf("HeapObjects: %d\n", stats.HeapObjects)
}

2. 内存引用追踪方法

使用pprof和runtime包可以追踪内存引用:

package main

import (
    "net/http"
    _ "net/http/pprof"
    "runtime"
    "runtime/debug"
    "time"
)

func allocateMemory() {
    // 模拟内存分配
    data := make([][]byte, 10000)
    for i := 0; i < 10000; i++ {
        data[i] = make([]byte, 1024)
    }
    time.Sleep(time.Hour) // 保持引用
}

func main() {
    // 启动pprof
    go func() {
        http.ListenAndServe("localhost:6060", nil)
    }()
    
    // 设置内存策略
    debug.SetGCPercent(100)
    
    go allocateMemory()
    
    // 定期强制GC和释放内存
    ticker := time.NewTicker(30 * time.Second)
    defer ticker.Stop()
    
    for range ticker.C {
        runtime.GC()
        debug.FreeOSMemory()
    }
}

3. 使用pprof进行内存分析

# 获取heap profile
go tool pprof http://localhost:6060/debug/pprof/heap

# 获取goroutine信息
go tool pprof http://localhost:6060/debug/pprof/goroutine

# 获取allocs profile(分配历史)
go tool pprof http://localhost:6060/debug/pprof/allocs

4. 详细内存分析示例

package main

import (
    "fmt"
    "os"
    "runtime"
    "runtime/debug"
    "runtime/pprof"
    "time"
)

func main() {
    // 创建heap profile文件
    f, err := os.Create("heap.pprof")
    if err != nil {
        panic(err)
    }
    defer f.Close()
    
    // 设置内存分析
    runtime.MemProfileRate = 1
    
    // 模拟工作负载
    go func() {
        cache := make(map[int][]byte)
        for i := 0; i < 1000000; i++ {
            cache[i%1000] = make([]byte, 1024)
            if i%10000 == 0 {
                time.Sleep(time.Millisecond)
            }
        }
    }()
    
    time.Sleep(5 * time.Second)
    
    // 写入heap profile
    if err := pprof.WriteHeapProfile(f); err != nil {
        panic(err)
    }
    
    // 读取内存统计
    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    
    fmt.Printf("HeapSys: %d\n", m.HeapSys)
    fmt.Printf("HeapInuse: %d\n", m.HeapInuse)
    fmt.Printf("HeapIdle: %d\n", m.HeapIdle)
    fmt.Printf("HeapReleased: %d\n", m.HeapReleased)
    
    // 强制GC和内存释放
    runtime.GC()
    debug.FreeOSMemory()
    
    runtime.ReadMemStats(&m)
    fmt.Printf("After GC - HeapReleased: %d\n", m.HeapReleased)
}

5. 内存保留的原因

即使使用GODEBUG=madvdontneed=1,Go运行时也可能保留内存:

  • 内存碎片化导致无法释放完整页面
  • 运行时内部结构占用
  • 未达到scavenge回收阈值
  • 保留内存以供未来分配使用
// 检查内存碎片
func analyzeFragmentation() {
    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    
    fragmentation := float64(m.HeapIdle-m.HeapReleased) / float64(m.HeapIdle) * 100
    fmt.Printf("内存碎片率: %.2f%%\n", fragmentation)
    fmt.Printf("空闲但未释放: %d bytes\n", m.HeapIdle-m.HeapReleased)
}

使用go tool pprof分析heap profile可以显示具体的内存分配调用栈,帮助识别内存引用来源。

回到顶部