有多个问题(与您的问题有关):
-
ps
(and top
等)显示多个内存读数。唯一感兴趣的通常称为RES or RSS https://en.wikipedia.org/wiki/Resident_set_size。你不知道那是哪一个。
基本上,看看通常命名的阅读材料VIRT
没意思。
-
正如沃尔克所说,pprof
不测量内存消耗,它测量(以您运行它的模式)内存分配率,即“多少”,而不是“频率”。
要理解它的含义,请考虑如何pprof
作品。
在分析过程中,计时器会滴答作响,每次滴答时,分析器都会对正在运行的程序进行快照,扫描所有活动 goroutine 的堆栈,并将堆上的活动对象属性赋予这些堆栈的堆栈帧中包含的变量,以及每个堆栈框架属于活动功能。
这意味着,如果您的进程将调用,例如,os.ReadFile
—根据其约定,分配足够长的字节片以包含要读取的文件的全部内容,—每次读取 1 GiB 文件 100 次,分析器的计时器将设法精确定位这 100 次调用中的每一次(它在采样时可能会错过一些调用),os.ReadFile
将归因于已分配 100 GiB。
But如果您的程序的编写方式不是保存这些调用返回的每个切片,而是对这些切片执行某些操作并在处理后将它们丢弃,则过去调用的切片可能已经被 GC 收集到分配新的时。
-
虽然不要求the spec https://golang.org/ref/spec,Go 的两个“标准”当代实现——最初被称为“gc”的一个,大多数人认为它是the实现,以及 GCC 前端 - 具有与您自己的进程流并行运行的垃圾收集器;它实际收集您的进程产生的垃圾的那一刻是由一组复杂的启发式控制的(开始here https://tip.golang.org/doc/gc-guide如果感兴趣的话)它会尝试在 GC 花费 CPU 时间和不执行 GC 花费 RAM 之间取得平衡;-),这意味着对于短期进程,GC 可能不会启动一次,这意味着您的进程将以所有生成的垃圾仍然浮动,并且当进程结束时,所有内存都将由操作系统以通常的方式回收。
-
当GC收集垃圾时,释放的内存并不会立即返回给操作系统。相反,涉及两阶段过程:
基本上,这意味着即使 GC 释放了一些内存,您也不会在正在运行的 Go 进程之外看到它,因为这些内存首先会重新调整到进程自己的池中。
-
不同版本的 Go(同样,我的意思是“gc”实现)实施了关于将释放的页面返回到操作系统的不同策略:首先,它们被标记为madvise(2) https://manpages.debian.org/2/madvise as MADV_FREE
,那么作为MADV_DONTNEED
然后再次作为MADV_FREE
。
如果您碰巧使用运行时将释放的内存标记为的 Go 版本MADV_DONTNEED
,读数RSS https://en.wikipedia.org/wiki/Resident_set_size会更不明智,因为以这种方式标记的内存仍然对进程不利'RSS https://en.wikipedia.org/wiki/Resident_set_size尽管操作系统被暗示它可以在需要时回收该内存。
回顾一下。
这个话题足够复杂,你似乎太快得出某些结论了;-)
更新。我决定稍微扩展一下内存管理,因为我觉得你头脑中的这些东西的大局中可能缺少某些点点滴滴,正因为如此,你可能会发现对你的问题的评论是毫无意义和不屑一顾的。
建议不要测量用 Go 编写的程序的内存消耗的原因ps
, top
和朋友们的根源在于内存管理的实现运行时环境 https://en.wikipedia.org/wiki/Runtime_system用当代高级编程语言编写的程序与操作系统内核及其运行的硬件中实现的底层内存管理相去甚远。
让我们考虑一下 Linux 有具体的例子。
你当然可以直接要求内核为你分配一块内存:mmap(2) https://manpages.debian.org/2/mmap is a syscall https://en.wikipedia.org/wiki/System_call这样做的。
如果你用MAP_PRIVATE
(通常也与MAP_ANONYMOUS
),内核将确保进程的页表有一个或多个新条目pages https://en.wikipedia.org/wiki/Page_(computer_memory)内存以包含您所请求的字节数的连续区域,并返回序列中第一页的地址。
这时候你可能会认为RSS https://en.wikipedia.org/wiki/Resident_set_size您的进程已增长了该字节数,但事实并非如此:内存是“保留”的,但实际上并未分配;为了真正分配内存页面,进程必须“接触”页面内的任何字节 - 通过读取或写入它:这将在 CPU 和内核处理程序上生成所谓的“页面错误”会要求硬件实际分配一个真正的“硬件”内存页。只有在那之后,该页面才会真正计入该进程'RSS https://en.wikipedia.org/wiki/Resident_set_size.
好吧,这很有趣,但您可能会看到一个问题:使用完整的页面进行操作不太方便(在不同的系统上,页面的大小可能不同;通常在 x86 谱系的系统上为 4 KiB):当您在高级语言,你不会在这么低的层面上思考记忆;相反,您期望正在运行的程序以某种方式实现您需要的“对象”(我在这里并不是指 OOP;只是包含某些语言或用户定义类型的值的内存片段)。
这些对象可以是任何大小,大多数时候比单个内存页小,而且更重要的是,大多数时候您甚至不会考虑这些对象在分配时消耗了多少空间。
即使使用 C 语言(如今被认为是相当低级的语言)进行编程,您通常也习惯于在 C 语言中使用内存管理函数。malloc(3) https://manpages.debian.org/3/malloc系列由标准 C 库提供,允许您分配任意大小的内存区域。
解决此类问题的一种方法是使用更高级别的内存管理器on top内核可以为你的程序做什么,事实上,每一个用高级语言(甚至 C 和 C++!)编写的通用程序都在使用一种: 对于解释性语言(例如 Perl、Tcl、Python 、POSIX shell 等)由解释器提供;对于字节编译语言(例如 Java),它由执行该代码的进程提供(例如 Java 的 JRE);对于编译为机器 (CPU) 代码的语言(例如 Go 的“库存”实现),它由包含在生成的可执行映像文件中的“运行时”代码提供,或者在将其加载到程序中时动态链接到程序中。用于执行的内存。
此类内存管理器通常非常复杂,因为它们必须处理许多复杂的问题,例如内存碎片,并且它们通常必须尽可能避免与内核对话,因为系统调用很慢。
后一个要求自然意味着进程级内存管理器尝试缓存它们曾经从内核获取的内存,并且不愿意将其释放回来。
所有这些意味着,比如说,在一个典型的active去编程你可能会疯狂记忆搅动— 成群的小对象一直被分配和释放,这对值几乎没有影响RSS https://en.wikipedia.org/wiki/Resident_set_size从进程的“外部”进行监控:所有这些扰动都由进程内内存管理器和(如普通 Go 实现的情况)处理,GC 自然与 MM 紧密集成。
因此,为了对长期运行的生产级 Go 程序中发生的情况有有用的、可操作的想法,此类程序通常提供一组不断更新的metrics(传送、收集和监控它们称为遥测)。对于 Go 程序,负责生成这些指标的程序的一部分可以定期调用runtime.ReadMemStats https://pkg.go.dev/runtime#ReadMemStats and runtime/debug.ReadGCStats https://pkg.go.dev/runtime/debug#ReadGCStats或者直接使用什么runtime/metrics https://pkg.go.dev/runtime/metrics必须提供。在 Zabbix、Graphana 等监控系统中查看此类指标非常有启发性:您可以从字面上看到进程内 MM 可用的空闲内存量在每个 GC 周期后如何增加,而RSS https://en.wikipedia.org/wiki/Resident_set_size保持大致相同。
另请注意,您可能会考虑在特殊环境变量中使用各种与 GC 相关的调试设置来运行 Go 程序GODEBUG
描述here https://pkg.go.dev/runtime#hdr-Environment_Variables:基本上,你让 Go 运行时为你正在运行的程序提供动力,发出有关 GC 如何工作的详细信息(另请参阅this https://dave.cheney.net/2014/07/11/visualising-the-go-garbage-collector).
希望这会让您好奇并进一步探索这些问题;-)
你可能会发现this https://povilasv.me/go-memory-management/很好地介绍了 Go 运行时实现的内存管理——与内核和硬件相关;推荐阅读。