登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

Go runtime.ReadMemStats 为什么不能直接代表 RSS

来源:17golang原创

时间:2026-10-23 03:37:33 423浏览 收藏

Go 程序里调用 runtime.ReadMemStats 后,最容易出现一个误判:把 Sys 或 HeapAlloc 当成进程 RSS。它们不能直接代表 RSS,因为统计对象不同。HeapAlloc 更接近当前仍被 Go 堆对象使用的空间,Sys 记录的是 Go runtime 从操作系统获得并用于堆、栈及内部结构的地址空间,而 RSS 是操作系统看到的、当前驻留在物理内存中的进程页。

排查 Go 内存时,堆对象和 GC 问题看 MemStats 或 pprof;进程是否逼近机器或容器内存上限看 RSS 或 cgroup 指标。两条线要在同一时间点采集、并列展示,不能用一个字段换算另一个指标。

MemStats 和 RSS 统计的不是同一层

ReadMemStats 返回的是 Go 内存分配器的快照,适合回答“堆对象有多少、runtime 保留了多少、GC 最近怎样”。RSS 则回答“这个进程当前有多少页面驻留在物理内存”。一个是运行时内部视角,一个是操作系统进程视角。

指标它主要说明什么不能直接推出什么
HeapAlloc当前已分配且仍属于 Go 堆的对象字节数不能推出整个进程 RSS
HeapSys为堆取得的虚拟地址空间,含尚未使用或已归还物理页的部分不能当作已占用物理内存
SysGo runtime 为堆、栈和内部结构取得的总量不能当作 OS 进程工作集
HeapReleased已经归还给操作系统、尚未重新取得的堆物理内存不能单独说明 RSS 会立即下降多少
RSS进程当前驻留的物理页面不能单独定位是哪段 Go 代码分配的
Go runtime MemStats 与操作系统 RSS 统计层级说明图
图1:MemStats 与 RSS 的统计层级说明图,不是截图或运行证据。

先用 ReadMemStats 看清 Go runtime 自己

下面的采集函数只回答运行时问题:活跃堆、堆保留量、已归还量、栈和总量。注意 Sys 的单位是字节,且它描述的是 runtime 的地址空间保留;不要把它改名为 rss 写进监控。

package main

import (
    "fmt"
    "runtime"
)

func printRuntimeMemory() {
    var m runtime.MemStats
    // 读取调用时刻的 Go 分配器统计,不把字段伪装成 RSS。
    runtime.ReadMemStats(&m)

    fmt.Printf("heap_alloc=%d MiB\\n", m.HeapAlloc/1024/1024)
    fmt.Printf("heap_sys=%d MiB\\n", m.HeapSys/1024/1024)
    fmt.Printf("heap_idle=%d MiB\\n", m.HeapIdle/1024/1024)
    fmt.Printf("heap_released=%d MiB\\n", m.HeapReleased/1024/1024)
    fmt.Printf("stack_sys=%d MiB\\n", m.StackSys/1024/1024)
    fmt.Printf("runtime_sys=%d MiB\\n", m.Sys/1024/1024)
}

这组字段适合看趋势。例如 HeapAlloc 在 GC 后仍持续上升,才值得沿着堆对象和引用链继续查;HeapIdle - HeapReleased 较大,常见解释是 runtime 暂时保留了可复用的空闲 span,并不自动等于泄漏。

再在同一时刻读取进程 RSS

Linux 上可以读取当前进程的 /proc/self/status,其中 VmRSS 是操作系统进程视角的驻留集。这个示例是 Linux 特定实现,生产代码应把它封装成平台适配层;macOS、Windows 和容器运行时的采集入口并不相同。

//go:build linux

package main

import (
    "bufio"
    "fmt"
    "os"
    "strconv"
    "strings"
)

func readLinuxRSS() (uint64, error) {
    f, err := os.Open("/proc/self/status")
    if err != nil {
        return 0, err
    }
    defer f.Close() // 读取完成后关闭 proc 文件描述符。

    scanner := bufio.NewScanner(f)
    for scanner.Scan() {
        fields := strings.Fields(scanner.Text())
        if len(fields) == 3 && fields[0] == "VmRSS:" && fields[2] == "kB" {
            kb, err := strconv.ParseUint(fields[1], 10, 64)
            if err != nil {
                return 0, err
            }
            return kb * 1024, nil // /proc 的 VmRSS 单位是 kB。
        }
    }
    if err := scanner.Err(); err != nil {
        return 0, err
    }
    return 0, fmt.Errorf("VmRSS not found")
}

把 printRuntimeMemory 和 readLinuxRSS 放在同一次采样里,监控上至少保留 heap_alloc、runtime_sys、stack_sys、heap_reclaimable 和 rss 五条曲线。图中的数值关系是解释用的静态结构,不代表某次真实运行结果。

Go HeapAlloc、空闲保留、StackSys 与 RSS 差异的诊断关系图
图2:Go 内存差异诊断关系图,展示排查方向而非真实采样结果。

用指标组合判断差异来源

不要看到 RSS 大于 HeapAlloc 就直接下结论。可以按组合关系缩小范围:

  • HeapAlloc 与 RSS 一起持续上涨:优先用 heap profile、对象数量和引用链检查 Go 堆是否有长期存活对象。
  • HeapAlloc 回落,但 RSS 仍高:对照 HeapIdle - HeapReleased;runtime 可能保留可复用页面,也要看回收节奏和内存上限。
  • StackSys 或 goroutine 数量上升:检查 goroutine 泄漏、阻塞调用和线程栈;这部分不应归因给普通堆对象。
  • RSS 明显高,Go runtime 指标解释不了:转向 cgo 分配、直接 mmap、共享库映射和平台层内存统计;runtime/metrics 也明确有些 cgo 或 syscall 直接映射不在 Go runtime 的内存类别内。

容器环境还要把进程 RSS 与 cgroup 的内存使用口径对齐。告警阈值如果针对容器限制,就读取对应 cgroup 指标;如果要定位 Go 堆泄漏,则保留 MemStats 和 pprof。两个目标不同,采集源也应不同。

监控与修正的采用边界

实际落地可以记住三条:第一,用 ReadMemStats 解释 Go 堆和 GC,不把 Sys 当 RSS;第二,用操作系统或容器指标判断进程是否接近物理内存上限;第三,只有在两个指标同一时间点采集时,差值才有排查价值。Go 官方诊断文档也把 ReadMemStats 放在运行时统计,把 heap profile 放在定位分配来源的工具链中。

如果目标是进一步统一 Go runtime 侧的指标,可考虑 runtime/metrics;但它仍然不是跨平台 RSS API。对于“RSS 为什么比 MemStats 大”的问题,正确修正不是找一个神奇换算公式,而是先确认额外驻留页属于栈、runtime 元数据、cgo、mmap 还是共享映射,再选择对应工具。

常见追问

HeapSys 比 HeapAlloc 大很多就是内存泄漏吗

不一定。HeapSys 包含虚拟地址空间和可复用的堆区域,先看 HeapIdle、HeapReleased、GC 后的 HeapAlloc 趋势,再用 heap profile 定位长期存活对象。

为什么 RSS 降得比 HeapAlloc 慢

对象释放只说明 Go 堆不再需要这些对象;runtime 可能暂时保留空闲 span,操作系统也按页面回收和映射情况反映 RSS。应同时观察 HeapReleased、空闲差值和进程 RSS,不要只看单个采样点。

go
声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>