Go runtime.ReadMemStats 为什么不能直接代表 RSS
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 | 为堆取得的虚拟地址空间,含尚未使用或已归还物理页的部分 | 不能当作已占用物理内存 |
Sys | Go runtime 为堆、栈和内部结构取得的总量 | 不能当作 OS 进程工作集 |
HeapReleased | 已经归还给操作系统、尚未重新取得的堆物理内存 | 不能单独说明 RSS 会立即下降多少 |
| RSS | 进程当前驻留的物理页面 | 不能单独定位是哪段 Go 代码分配的 |

先用 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 五条曲线。图中的数值关系是解释用的静态结构,不代表某次真实运行结果。

用指标组合判断差异来源
不要看到 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 trace 和 pprof 应该先看哪一个
- 上一篇
- Go trace 和 pprof 应该先看哪一个
- 下一篇
- Go GC 控制参数改了为什么延迟反而升高
-
- Golang · Go问答 | 59分钟前 | 垃圾回收 · Go问答 · Go 延迟 GC GOGC GOMEMLIMIT
- Go GC 控制参数改了为什么延迟反而升高
- 333浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go trace 和 pprof 应该先看哪一个
- 179浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go slog JSON 日志为什么混入不可序列化字段
- 470浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go time.Timer.Stop 返回 false 后怎样避免重复处理到期事件
- 246浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go 服务器优雅退出为什么仍有请求被截断
- 451浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go http.Request FormValue 为什么会吞掉解析错误
- 129浏览 收藏
-
- Golang · Go问答 | 1小时前 | 性能排查 · Go问答 · Go io.Writer 短写 WriteTo net.Buffers
- Go net.Buffers WriteTo 为什么可能只写出部分数据
- 196浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go WaitGroup Add 放错位置为什么会 panic
- 325浏览 收藏
-
- Golang · Go问答 | 1小时前 |
- Go 反射设置字段失败通常缺哪一步
- 264浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 600次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 641次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 673次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 639次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 458次使用
-
- Go map 并发写 panic 怎么办:从共享 map 到可控写入路径
- 2026-06-30 123浏览
-
- go语言中的defer关键字
- 2023-02-17 150浏览
-
- Golang中Interface接口的三个特性
- 2023-01-07 394浏览
-
- go语言中函数与方法介绍
- 2023-01-07 297浏览
-
- go语言数据类型之字符串string
- 2022-12-30 321浏览

