Windows的「内存使用量」究竟表示什么 ── 正确解读 Working Set・Private Bytes・Commit・页面文件

· · Windows, Windows开发, 内存管理, Working Set, Private Bytes, Commit, 页面文件, 性能监控, 故障调查, Sysinternals

任务管理器中,某个进程的「内存」显示为 1.2GB。然而在 Process Explorer 中,Working Set 却是 1.5GB,Private Bytes 是 2.4GB,VMMap 中的 Size 更大。再看系统整体,则显示「已提交 19.6/31.8GB」。

那么,这个应用到底用了多少 GB 的内存?

答案是:要看的数字,取决于你想知道什么。是想知道当前载入 RAM 的量,还是想知道该进程固有的分配量,还是想知道系统承诺未来也会支撑的量,又或者只是想知道占用了多大范围的虚拟地址 ── 不同的问题,对应不同的指标。

Windows 的内存指标之所以令人困惑,是因为它们都被显示为「内存」这同一个词,但实际测量的却是以下这些不同的维度。

  • 占用了多少地址空间
  • 消耗了多少Commit
  • 当前是否常驻于物理 RAM
  • 该页面是进程固有的,还是可共享的
  • 系统整体还能支撑多少分配

本文面向在 Windows 10/11 及现行 Windows Server 上排查应用内存增长或系统整体内存不足问题的读者,将 Working Set、Private Working Set、Private Bytes、Commit、Virtual Bytes、页面文件、Available、页面错误(Page Fault)之间的关系整理成一张统一的图景。

关于追查 .NET 对象无法被回收的具体步骤,请参阅《在 .NET 中区分 GC 等待与内存泄漏》;VMMap 和 Process Explorer 的具体操作方法,则在《Process Explorer / Handle / VMMap 实战》中有详细介绍。本文将聚焦于作为这些内容前提的Windows OS 侧数字的读法

1. 先说结论

  • Working Set 是当前常驻在 RAM 中的页面。它不仅包括进程固有的页面,还包括 DLL 代码、内存映射文件等可与其他进程共享的页面。1
  • Private Working Set 是 Working Set 中当前仅属于该进程本身的部分。可以把它当作「该进程目前固有占用的 RAM」的近似值,但它并不是应用所分配的全部内存量。2
  • Private Bytes 是该进程固有的 Commit 量。这是一个与「当前是否载入 RAM」无关的独立指标。Win32 API 结构体中的 PagefileUsage 字段,在当前的 Windows 上实质上表示的也是同一个 Commit Charge,并不是实际写入页面文件的字节数。2
  • 任务管理器中的「已提交 X/Y」,X 是系统整体当前的 Commit 量,Y 是 Commit 上限。X 并不是页面文件的使用量。Y 大致由 RAM 与页面文件的总和决定。3
  • Reserve 与 Commit 是不同的概念。仅仅 Reserve 了虚拟地址,只是为将来使用而预留了这段范围,并不会因此消耗同等数量的 RAM 或 Commit 上限。45
  • 页面错误(Page Fault)未必意味着磁盘 I/O。页面错误分为可在 RAM 内解决的软错误(soft fault),以及需要从页面文件、可执行文件、内存映射文件等处读取的硬错误(hard fault)。16
  • 内存泄漏不应根据单次数值判断,而应根据反复施加相同负载时的变化趋势来判断。尤其要观察 Private Bytes 及其内部构成,是否在处理结束后仍逐步持续增长,无法回到相同的稳定状态。

如果用一句话概括:Working Set 是「当前 RAM 中的量」,Private Bytes 是「该进程固有承诺的量」,Commit 是「系统整体承诺的量」

选择Windows的主要内存指标想知道的对象是RAM常驻量、进程固有的Commit量、系统整体的Commit量,还是虚拟地址范围,决定了应该查看的指标当前RAM中的量进程固有的承诺量系统整体的承诺量已分配的地址范围「内存使用量」想知道什么Working SetPrivate BytesSystem CommitVirtual Bytes / Reserved常驻于物理RAM进程固有的Commit与Commit Limit比较虚拟地址空间

图1:将「内存偏多」这一观察,首先分解为四种问题。

2. 把「内存使用量」拆分为四个维度

首先,不要把 Windows 内存当作「一根柱子」,而要按四个维度来考虑。

对一个页面进行分类的四个独立维度分别确认虚拟地址的状态、Commit页面的后备存储、是否常驻物理RAM、能否与其他进程共享按四个维度看一个页面地址状态Free / Reserved / Committed后备存储页面文件后备 / 文件后备RAM常驻常驻 / 未常驻可共享性Private / Shareable

图2:即使是同一个页面,地址状态・后备存储・常驻・共享性也是分别决定的。

Mapped 并不是与 Free・Reserved・Committed 并列的地址状态,而是区域的一种类型。映射视图(mapped view)的页面同样可以处于 Committed 状态。另外,Private 也不是后备存储媒介,而是可共享性的分类。因此,后备存储要单独判断是页面文件后备(page-file-backed)还是文件后备(file-backed),可共享性也要单独判断是 Private 还是 Shareable。

把这四个维度组合起来,主要指标之间的关系如下表所示。

页面状态 Working Set Private Working Set Private Bytes Virtual Bytes 系
进程固有・已 Commit・RAM 常驻 包含 包含 包含 包含
进程固有・已 Commit・RAM 未常驻 不包含 不包含 包含 包含
DLL 或映射文件的共享页面・RAM 常驻 包含 原则上不包含 原则上不包含 包含
已保留但未 Commit 不包含 不包含 不包含 可能包含
未使用的地址范围 不包含 不包含 不包含 通常不包含
页面类型与主要内存指标的对应关系展示Private且常驻的页面、Private且非常驻的页面、共享且常驻的页面、仅保留的范围分别计入哪些指标Private・已Commit・RAM常驻Private・已Commit・RAM未常驻共享页面・RAM常驻Reserved・未CommitWorking SetPrivate Working SetPrivate BytesVirtual Bytes系

图3:Working Set 与 Private Bytes 统计的是不同的页面集合,因此不构成简单的包含关系。

这里需要注意的是,Working Set 与 Private Bytes 之间并不是简单的包含关系

Private Bytes 中包含了进程固有、但目前并未常驻于 RAM 的页面。而 Working Set 中则包含了 DLL 代码、共享内存等 Private Bytes 不会计入的共享页面。因此,根据进程和时间点的不同,Working Set 可能大于 Private Bytes,反过来也可能发生。

另外,如果单纯把多个进程的 Working Set 相加,可能会把共享 DLL 等相同的物理页面重复计算多次。「各进程的 Working Set 之和 = 使用中的 RAM」这种说法并不成立。

3. 虚拟地址空间 ── Reserve 与 Commit 是两回事

3.1. 虚拟地址并非物理 RAM 的地址

每个进程都拥有自己专属的虚拟地址空间。应用所操作的指针并不是直接指向物理 RAM 中的位置,而是由 Windows 通过页表(page table)把虚拟地址映射到物理页面或文件中的数据。7

正因如此,即使是搭载了 64GB RAM 的 PC,某个 32 位进程能使用的虚拟地址空间通常也会远小于这个数字。反过来,拥有比物理 RAM 更大的虚拟地址空间的 64 位进程也很常见。

3.2. Reserved 只是「占住了地址」

VirtualAllocMEM_RESERVE 会为将来使用而分配一段连续的虚拟地址范围。在这个阶段,页面还没有关联任何物理存储,也无法对这段范围进行读写。45

例如,即使数据库或运行时为将来的增长而 Reserve 了 8GB 的地址范围,单凭这一点也不会因此消耗 8GB 的 RAM 或 8GB 的 Private Bytes。

3.3. Committed 是「需要时会予以支撑」的承诺状态

MEM_COMMIT 是把该虚拟页面变为 Committed 状态、并承诺 Windows 会提供必要后备存储的操作。是否真的允许读取・写入・执行,则由 PAGE_READONLYPAGE_READWRITEPAGE_EXECUTEPAGE_NOACCESS 等页面保护属性另行决定,Committed 本身并不意味着「可读写」。Commit 完成的那一刻就会计入系统的 Commit Charge,但实际的物理页面有可能要到首次访问时才会被分配。首次被访问的页面会以零值初始化,经过 Demand-zero fault 后进入 Working Set。51

因此,同样是「已分配」,实际上分为以下三个阶段。

从Reserve到Commit再到RAM常驻的三个阶段展示预留虚拟地址、Commit页面、首次访问时分配物理页面并进入Working Set的流程MEM_COMMIT首次访问・Demand-zero fault未访问则MEM_RESERVE,分配地址范围反映在Virtual Bytes系中已Commit,依页面保护属性可访问反映在Private Bytes / System Commit中分配物理页面并常驻RAM反映在Working Set中已Commit但未常驻

图4:Reserve、Commit、首次访问是三个不同的事件,分别驱动不同的指标变化。

这三个阶段,分别驱动 Virtual Bytes 系、Private Bytes、Working Set 这几个数字各自变化。

3.4. 即使有空闲 RAM,也会发生 OutOfMemory 的原因

内存分配能否成功,并不仅仅取决于空闲 RAM。

  • 进程的虚拟地址空间已用尽
  • 没有所需大小的连续空闲地址范围
  • 系统整体的 Commit Charge 已达到 Commit Limit
  • Job Object、容器、运行时、库有各自独立的上限
  • 是 32 位进程
  • 原生堆(native heap)已经碎片化

即使在 64 位 Windows 上,32 位进程的用户模式虚拟地址空间,如果 IMAGE_FILE_LARGE_ADDRESS_AWARE 未启用,通常也只有 2GB。而启用了该标志的 32 位应用,在 64 位 Windows 上最多可以使用 4GB。8

因此,「PC 明明有 20GB 空闲 RAM,32 位应用却在 1.6GB 左右就分配失败」并不矛盾。这未必是 RAM 的问题,而很可能是撞上了地址空间的碎片化或上限。

4. Working Set ── 当前载入 RAM 的页面

Working Set 是进程的虚拟地址空间中,当前常驻于物理 RAM 的页面集合。1

其中混合了以下内容。

  • 进程固有的堆和栈
  • EXE 和 DLL 的代码・只读数据
  • 内存映射文件
  • 共享内存
  • Copy-on-write 之后变为该进程固有的页面
  • 运行时及各类库所触碰过的页面

4.1. Working Set 增大,未必意味着分配量增加

如果首次访问一个早已被 Commit 的页面,可能会出现 Private Bytes 不变、只有 Working Set 增大的情况。把一个大文件做内存映射并依次读取时,同样可能出现来自文件的页面进入 Working Set,而 Private Bytes 几乎不增加的情况。

反过来,当 Windows 因内存压力而对 Working Set 进行 Trim 时,应用在逻辑上仍持有同样的内存,却只是 Working Set 减小了。之后再次访问时,会经过页面错误重新载入。

因此,Working Set 下降未必意味着「应用释放了内存」,上升也未必意味着「应用新分配了内存」。

仅Working Set增减的典型流程同一批已Commit的页面在首次访问时进入RAM,因Trim变为非常驻,再次访问后恢复,期间Private Bytes始终被计入首次访问因内存压力被Trim再次访问触发Page Fault同一批已Commit页面常驻RAM非常驻计入Working Set不计入Working Set处于Commit状态期间计入Private Bytes

图5:Working Set 会随常驻状态上下波动,但只要同一页面的 Commit 仍然存在,Private Bytes 就不会减少。

4.2. Working Set 中包含共享页面

如果 10 个进程共享同一个 DLL 的代码页面,那么这些页面可能会出现在每个进程各自的 Working Set 中,但在物理 RAM 上实际上只存在一份。即使 Working Set 之和超过了搭载的 RAM 容量,也不能立即断定为异常。

如果想更接近「当前仅由该进程固有占用的 RAM」这一概念,可以查看 Private Working Set。不过,它同样不是「该进程分配的全部内存」,而始终只是当前常驻中的 Private 页面

4.3. 强行缩小 Working Set 并不能修复内存泄漏

使用 EmptyWorkingSetSetProcessWorkingSetSize,可以把页面从进程的 Working Set 中赶出去。但这并不是释放 Commit 或释放堆上引用的操作。可能出现 Private Bytes 不变、表面上的 RAM 使用量却下降,而下一次访问时页面错误增多的情况。9

如果只是在按下「减少内存」按钮后的瞬间,任务管理器上的数字变小,一旦重新开始操作就立刻恢复原状,那么这很可能不是「释放」,而只是对 Working Set 做了 Trim。

5. Private Bytes ── 进程固有的 Commit 量

Private Bytes 是专为该进程 Commit 的虚拟内存量。它表示无法与其他进程共享的 Commit Charge,与当前是否常驻 RAM 无关。在 Microsoft 的 PROCESS_MEMORY_COUNTERS_EX 中,PrivateUsage 对应的就是这个值。102

Win32 API 中还有一个容易引起误解的字段名 PagefileUsage,但现行文档将其定义为「该进程的 Commit Charge」,并说明它与 PrivateUsage 的值相同。也就是说,Private Bytes 为 2GB,并不意味着「已向 pagefile.sys 写入了 2GB」。2

以下是对 Private Bytes 产生影响的代表性因素。

  • HeapAllocmallocnew 等所使用的原生堆(native heap)的 Commit
  • 通过 VirtualAlloc 直接 Commit 的 Private Data
  • .NET GC 堆中已 Commit 的区域
  • 线程栈中实际已 Commit 的部分
  • Copy-on-write 视图(FILE_MAP_COPY)在映射时为整个视图预先分配的 Commit Charge
  • 库或设备 SDK 内部所持有的 Private 缓冲区

通过 FILE_MAP_COPY 创建的 Copy-on-write 视图中,由于每个页面将来都有可能变为 Private,Windows 会在映射的那一刻就为整个视图预留 Commit Charge,以便能够用页面文件作为后备。因此,即使实际写入、生成 Private 副本之前,System Commit 与该进程的 Commit Charge(Private Bytes)也可能会按整个视图的大小增加。11

5.1. 为什么 free 或 GC 之后 Private Bytes 也不会下降

即使从应用的角度看已经「释放」了内存,运行时或堆分配器也可能不会把这块区域 Decommit 归还给 OS,而是将其保留下来以便将来复用。在这种情况下,即便应用内部已经可以复用该区域,Private Bytes 依然会维持在较高水平。

此外,大区域中只有一部分仍在使用、发生了碎片化、缓存或池已经预热到上限等原因,同样会导致数值居高不下。

因此,仅凭 Private Bytes 数值高,并不能证明存在内存泄漏。应该观察的是以下这种时间差比较。

  1. 把相同的处理重复相同的次数
  2. 处理结束后等待相同的时长
  3. 确认 Private Bytes 是否回落到相同水平,或是在某个固定值处封顶
  4. 用 VMMap 或堆转储(heap dump)确认究竟是哪个区域・哪种类型增长了
为什么free或GC之后Private Bytes也不会下降分配器是把应用不再需要的区域归还给OS,还是保留下来以便复用,会导致Private Bytes的变化不同Decommit / Release保留以便复用应用通过free / GC使区域变为不再需要分配器是否归还给OSCommit Charge减少Private Bytes下降区域保持Commit状态Private Bytes居高不下池・缓存・碎片化

图6:应用内部变为可复用,与把 Commit 归还给 OS,并不是一回事。

5.2. 高度疑似内存泄漏的形态

以下这种每次负载后底部都往上抬升的「阶梯状」增长,需要特别留意。

Private Bytes
  ^
  |                    ________
  |             ______|
  |      ______|
  |_____|
  +----------------------------> 重复相同的处理

不过,即使呈阶梯状,也可能只是因为首次 JIT、字体、图像解码器、连接池、缓存的预热而增长几次,之后便趋于稳定。比起是否在增长,更重要的是是否无法收敛到稳定状态

6. System Commit ── 「已提交 X/Y」的真面目

任务管理器【性能】→【内存】中的「已提交 X/Y」,是系统整体的指标。

  • X:System Commit Charge 当前 Windows 在系统整体范围内承诺会支撑的已 Commit 内存
  • Y:System Commit Limit 系统所能支撑的 Commit 量上限

Commit Limit 大致由物理 RAM 与所有页面文件的总和决定。如果没有页面文件,则会略小于所搭载的 RAM。36

System Commit Charge与Commit Limit的关系各进程固有、共享节区、内核的Commit构成当前值X,物理RAM与页面文件支撑上限YX不能超过Y各进程的Private CommitSystem Commit Charge,X以页面文件为后备的共享节区Commit内核的Commit物理RAMSystem Commit Limit,Y页面文件

图7:X 是当前的承诺量,Y 是能够支撑该承诺的上限,两者都不是页面文件使用量的显示。

System Commit Charge 中,不仅包含各进程 Private Bytes 的总和,还包含以页面文件为后备的共享节区(section)的 Commit,以及内核所消耗的 Commit。因此,仅凭各进程 Private Bytes 的总和,无法完整解释 X 的数值。

6.1. Commit Charge 并非页面文件使用量

设想一台系统:RAM 16GB,页面文件 16GB,Committed 显示为 20/31GB。

这里的 20GB 并不意味着「已向页面文件写入了 20GB」。这 20GB 是指,对于 Private 的、可修改的页面等,Windows 承诺在需要时会准备好 RAM 或页面文件等后备存储的总量。

在这一时刻,以下几种状态是混合共存的:

  • 大部分常驻在 RAM 中
  • 一部分已经被换出到页面文件
  • 已经 Commit,但尚未经历首次访问
  • 作为内核侧的 Commit 被消耗

如果想查看页面文件的实际使用率,需要在 Commit 之外另行查看 Paging File(*)\% Usage。不过 Microsoft 的资料同样指出,仅凭页面文件使用率高,并不能断定就是性能问题,应结合是否达到 Commit Limit、Modified Page List、实际的分页 I/O 一并判断。6

6.2. 接近 Commit Limit 时会发生什么

当 System Commit Charge 达到 Commit Limit 时,系统就无法再支撑新的 Commit 请求,会导致进程内存分配失败、应用崩溃、操作无法进行等后果。3

这时候,比起「空闲 RAM」,Commit 的 X/Y 更为重要。即使通过 Trim Working Set 腾出了空闲 RAM,只要 Commit Charge 本身没有减少,达到 Commit Limit 的问题就不会解除。

6.3. 页面文件的三个作用

页面文件主要具备以下作用。

  1. 扩展 Commit Limit
  2. 使不常用的已修改页面能够从 RAM 中换出
  3. 根据配置为系统崩溃转储(crash dump)提供支撑

禁用页面文件,并不是「磁盘 I/O 必然减少、系统必然变快」这么简单的关系。相反,它可能会降低 Commit Limit,使得已修改但暂时不用的页面更容易滞留在 RAM 中,并可能导致崩溃时无法采集所需的转储文件。36

页面文件的合适大小,并不能仅凭所搭载的 RAM 容量来决定。Microsoft 也说明,由于峰值时的 System Commit Charge 以及所需的崩溃转储类型因系统而异,无法一概而论。6

7. 物理 RAM 的构成 ── 不能仅凭 Available 偏低就下判断

物理 RAM 并不只是被用户进程的 Working Set 所占用。

  • 各进程的 Working Set
  • 系统文件缓存
  • Standby、Modified、Free、Zeroed 等页面列表
  • 内核的 Paged Pool / Nonpaged Pool
  • 设备驱动程序所持有的内存
  • 内存压缩的存储区
  • 与 GPU 或设备共享・保留的区域
  • 硬件保留区域

7.1. Available 中也包含可复用的缓存

Windows 的 Available MBytes 并不仅仅是完全未使用的 RAM。它是一个既包含 Free、Zeroed,也包含必要时可以复用的 Standby 页面在内的指标。12

  • Free:当前尚未分配给任何用途的页面
  • Zeroed:已完成清零、可以安全交给其他进程的页面
  • Standby:已从 Working Set 中移除,但内容仍缓存在 RAM 上的页面
  • Modified:内容已被修改,在复用之前需要先写回相应后备存储的页面
Working Set与页面列表之间的迁移使用中的页面若未修改则移至Standby,若已修改则移至Modified,再经过重新访问、写回、复用等流程移出未修改页面移出已修改页面写回完成重新访问复用于其他用途清零分配后被访问Working Set,使用中Standby,保留内容的复用候选Modified,等待写回分配给其他用途Free,未使用Zeroed,可供新分配计入Available

图8:Available 中不仅包含完全空闲的部分,也包含必要时可复用的 Standby。

「为了增加空闲 RAM 而丢弃所有缓存」并不总是划算的做法。如果 Standby 上还保留着需要的数据,再次访问时就无需读取磁盘,可以快速地回到 Working Set 中。

因此,即使在任务管理器中看到 Free 很少,只要 Available 充足,并且硬页面错误或磁盘等待没有构成问题,那也许只是 Windows 把 RAM 当作缓存有效利用而已。

7.2. 没有大进程、RAM 却在减少的情况

把各进程的 Private Working Set 全部加起来也无法解释的内存消耗,并不罕见。

  • 文件缓存或内存映射文件
  • Nonpaged Pool / Paged Pool
  • 驱动程序锁定的页面
  • 共享页面
  • 内存压缩
  • 虚拟化或 GPU 相关的分配

这种情况下,与其一直盯着进程列表看,不如用 Sysinternals 的 RAMMap 查看 Use Counts、Processes、Priority Summary、File Summary。RAMMap 是官方提供的、用来按用途・页面列表・文件对物理内存进行分解的工具。13

如果只有 Nonpaged Pool 在持续增长,那么此时应该怀疑的就不是用户模式应用的 Private Bytes,而是驱动程序或内核侧的泄漏了。

8. Page Fault ── 数量多并不代表异常

当进程访问一个当前不在 Working Set 中的页面时,就会发生 Page Fault。虽然名字里带有「Fault」这个词,但它并不是异常故障,而是驱动虚拟内存运转的常规机制。1

8.1. 软页面错误(Soft Page Fault)

指的是无需读取磁盘就能解决的页面错误。

  • 页面仍保留在 Standby 或 Transition 状态
  • 其他进程的 Working Set 中已存在相同的共享页面
  • 首次访问已 Commit 的页面,需要分配零页面
  • 已经通过内存管理器的预读(read-ahead)载入 RAM

因此,即使 \Memory\Page Faults/sec 的数值很大,也未必意味着发生了磁盘 I/O 或延迟。

8.2. 硬页面错误(Hard Page Fault)

指的是需要从磁盘上的后备存储(backing store)读取内容的页面错误。读取来源并不限于页面文件。

  • .exe.dll 的代码・数据
  • 内存映射文件
  • 页面文件
软页面错误与硬页面错误的分支访问不在Working Set中的页面时,如果不需要存储I/O就作为软页面错误处理,如果需要则作为硬页面错误处理否,Standby・共享・Demand-zero等访问不在Working Set中的页面是否需要存储I/O软页面错误不读取磁盘,直接进入Working Set硬页面错误从哪里读取EXE / DLL内存映射文件页面文件读取后进入Working Set

图9:仅凭 Page Fault 这个名称,无法判断是否发生了磁盘 I/O。

Microsoft 列举了用于衡量硬错误的计数器,包括 \Memory\Pages/sec\Memory\Page Reads/sec\Memory\Pages Input/sec 等。即使这些数值偏高,也未必就是内存不足,因此需要与 Available MBytes、磁盘延迟、实际响应时间进行关联分析。6

8.3. 不要设置统一的阈值

像「Page Faults/sec 超过 1000 就算异常」这样的固定数值,其含义会因存储介质、页面大小、工作负载、访问局部性的不同而改变。

在实务中,应该把以下指标放在同一时间轴上对比。

  • Memory\Available MBytes
  • Memory\Pages Input/sec
  • Memory\Page Reads/sec
  • 目标磁盘的 Read latency / Queue
  • 目标进程的 Working Set・Private Bytes
  • 应用的处理时间、超时、UI 响应

如果随着负载增加,Available 同步下降,Pages Input/sec 与磁盘等待同时上升,处理时间也在恶化,那么就具备了怀疑「物理内存压力导致分页」的充分依据。

9. 该在哪个界面・工具中查看什么

想知道的内容 首先查看的指标 主要工具
当前目标进程载入 RAM 的量 Working Set 任务管理器、Process Explorer、Get-Process
其中进程固有的 RAM Private Working Set / Working Set - Private 任务管理器的详细信息列、Process Explorer、PerfMon
目标进程固有的 Commit 量 Private Bytes / Commit Size Process Explorer、PerfMon、VMMap、Get-Process
进程的虚拟地址范围 Virtual Bytes / Size Process Explorer、VMMap、Get-Process
系统整体的 Commit 余量 Committed Bytes / Commit Limit 任务管理器【性能】、PerfMon
物理 RAM 的可复用余量 Available MBytes 任务管理器、PerfMon
Standby・Modified・文件缓存的构成 页面列表・按用途分类的构成 RAMMap
Private Bytes 中哪部分增长了 Heap / Private Data / Managed Heap 等 VMMap、WinDbg、各运行时专用转储工具
伴随磁盘的分页 Pages Input/sec、Page Reads/sec、磁盘延迟 PerfMon、WPR/WPA
选择Windows内存排查工具根据目标是单个进程还是系统整体、是单一时间点还是时间序列、是否要追踪到运行时内部,来选择使用的工具单一时间点的构成时间序列系统整体物理RAM的构成包含CPU・I/O・等待的时间轴.NET堆原生堆想要区分排查什么目标是单个进程吗单一时间点还是时间序列VMMapPerfMon / PowerShell物理RAM的构成还是时间轴RAMMapWPR / WPA是否要追踪到运行时内部的持有情况dotnet-dump / PerfViewWinDbg / Application Verifier

图10:先确定目标范围和时间轴,就能不多不少地选出所需的工具。

9.1. 任务管理器

在任务管理器中,应该分开不同的界面来看。

  • 【进程】或【详细信息】:各个进程的 Working Set 系、Commit Size 系指标
  • 【性能】→【内存】:系统整体的已用(In use)、可用(Available)、已提交(Committed)、缓存(Cached)、分页缓冲池(Paged pool)、未分页缓冲池(Non-paged pool)

不要只凭「内存」这一个列名来判断,应在【详细信息】选项卡的列标题上单击右键,添加 Working Set、Peak Working Set、Commit Size 等所需的列。由于列名会因 Windows 版本和显示语言的不同而略有差异,务必先确认列的含义,再进行记录

9.2. 用 PowerShell 采集时间序列

如果已知目标进程的 ID,可以用 Get-Process 同时采集 Working Set、Private Bytes、Virtual Bytes 的变化趋势。

param(
    [Parameter(Mandatory)]
    [int]$ProcessId,

    [int]$IntervalSeconds = 5,
    [int]$SampleCount = 60
)

$samples = for ($i = 0; $i -lt $SampleCount; $i++) {
    $process = Get-Process -Id $ProcessId -ErrorAction Stop

    [pscustomobject]@{
        Timestamp      = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
        ProcessId      = $process.Id
        WorkingSetMB   = [math]::Round($process.WorkingSet64 / 1MB, 1)
        PrivateBytesMB = [math]::Round($process.PrivateMemorySize64 / 1MB, 1)
        VirtualBytesMB = [math]::Round($process.VirtualMemorySize64 / 1MB, 1)
        Handles        = $process.HandleCount
        Threads        = $process.Threads.Count
    }

    Start-Sleep -Seconds $IntervalSeconds
}

$samples | Format-Table -AutoSize
$samples | Export-Csv .\memory-samples.csv -NoTypeInformation -Encoding utf8

.NETProcess.WorkingSet64 对应 Working Set,PrivateMemorySize64 对应 Private Bytes,VirtualMemorySize64 对应 Virtual Bytes。141516

对于存在多个实例的应用,请按 PID 而不是按名称来追踪。在需要跨越重启(PID 会因此变化)的长期监控场景中,必须记录启动时间或服务名称等信息,设计成不会认错目标的方案。

9.3. 用 PerfMon 把系统与进程放在同一时间轴上

至少同时记录以下内容,会更便于问题定位。

\Process(<目标进程>)\ID Process
\Process(<目标进程>)\Working Set
\Process(<目标进程>)\Working Set - Private
\Process(<目标进程>)\Private Bytes
\Process(<目标进程>)\Virtual Bytes

\Memory\Available MBytes
\Memory\Committed Bytes
\Memory\Commit Limit
\Memory\Pages Input/sec
\Memory\Page Reads/sec
\Memory\Pool Nonpaged Bytes
\Memory\Pool Paged Bytes

当存在多个同名进程,或者在监控期间发生了重启时,仅凭 Process(name)Process(name#N) 这样的实例名无法固定目标。应在每个采样点同时记录 ID Process,只采用其值与追踪目标 PID 一致的那个实例。如果跨越了会导致 PID 变化的重启,还需要另外记录切换发生的时间点。

Windows 的性能计数器名称有时会因显示语言的不同而被本地化。如果在 PowerShell 中直接指定英文名称却找不到,可以从 PerfMon 的图形界面添加计数器,或者用 Get-Counter -ListSet * 确认本地环境下的名称。

9.4. 不要混淆 VMMap 与 RAMMap 的角色

  • VMMap:将单个进程的虚拟内存与 Working Set,按 Heap、Image、Mapped File、Private Data、Managed Heap 等类型进行分解
  • RAMMap:将系统整体的物理 RAM,按用途、页面列表、进程、文件进行分解

「这个进程的 Private Bytes 是因为什么增长的」用 VMMap 排查,「进程列表无法解释的那部分 RAM 究竟用在了哪里」用 RAMMap 排查。1713

10. 从数字的组合中解读症状

观察到的形态 首先考虑的可能性 接下来应确认的内容
Working Set 增长,Private Bytes 稳定 首次访问已有页面、共享 DLL、映射文件、文件缓存 VMMap 的 Image / Mapped File、Pages Input/sec
Private Bytes 增长,Working Set 稳定 Private 的 Commit 增加了,但处于未常驻或已被 Trim 的状态 VMMap 的 Heap / Private Data / Managed Heap
两者都在启动后不久增长,之后趋于平稳 JIT、缓存、池、初始化的预热 追加相同负载后是否会再次增长
每次负载后 Private Bytes 的底部都在抬升 泄漏、无上限缓存、释放后仍持有内存的分配器 前后两次的 VMMap 快照、堆转储
只有 Working Set 骤然下降,操作后又恢复 OS 或应用对 Working Set 做了 Trim Private Bytes、Pages Input/sec、响应时间
Committed X/Y 中的 X 正在接近 Y 系统整体的 Commit 压力 Private Bytes 排名靠前的进程、Paged/Nonpaged Pool、页面文件设置
Available 偏低,Pages Input/sec 与磁盘延迟偏高 物理 RAM 压力与硬分页(hard paging) Working Set 排名靠前的进程、RAMMap、与工作负载的相关性
RAM 使用率高,但没有大进程 缓存、共享页面、内核池、驱动程序、压缩等 RAMMap、Pool Nonpaged/Paged Bytes
有空闲 RAM,但只有 32 位应用失败 虚拟地址空间上限・碎片化 VMMap 的 Free/Reserved、可执行文件的 LAA 设置
Private Bytes 偏高,但反复处理也不再增长 可能是保持高水位的池或缓存 上限、复用情况、达到峰值后的稳定性

这张表中最重要的一点是:不要只看单一数值,而要组合起来解读

11. 内存泄漏排查的实务步骤

11.1. 首先确定复现条件与稳定点

只说「几天内会增长」是无法进行比较的。

  • 启动后的预热要纳入多长时间
  • 一个周期的操作内容
  • 一个周期结束后等待多少秒
  • 需要多少次才能达到缓存上限
  • 正常版本与问题版本能否使用相同的输入

需要先确定以上这些内容。

11.2. 同时记录进程与系统

至少应在相同的时间点记录以下内容。

  • 目标进程的 Working Set
  • 目标进程的 Private Bytes
  • 目标进程的 Virtual Bytes
  • 系统的 Committed Bytes / Commit Limit
  • Available MBytes
  • Pages Input/sec
  • 句柄数、线程数
  • 操作次数或处理件数

如果进程的 Private Bytes 保持稳定,而系统的 Commit 却持续增长,就需要把视野扩大到其他进程、内核、驱动程序、共享节区等方面。

11.3. 先确定正在增长的是哪个「维度」

  • 仅 Working Set:常驻页面、共享・文件来源、Trim 与重新载入
  • Private Bytes:进程固有的 Commit
  • 仅 Virtual Bytes:Reserve、映射、地址空间碎片化
  • 仅 System Commit:也包含其他进程与内核侧
  • Nonpaged Pool:驱动程序・内核侧
  • Handles / GDI / USER:内存以外的资源泄漏

如果跳过这个顺序直接采集转储,就会在目标搞错的情况下阅读大量信息。

11.4. 深入到具体构成

  • 原生进程:VMMap、WinDbg、Application Verifier、堆跟踪(heap trace)
  • .NET:dotnet-countersdotnet-gcdumpdotnet-dump、PerfView
  • 系统整体:RAMMap、PerfMon、WPR/WPA
  • 内核池:PoolMon、WinDbg

VMMap 会按类型显示进程已 Commit 的虚拟内存,以及各类型分别分配到的 Working Set。能把 Private Bytes 的增长收窄到「Heap」「Private Data」「Managed Heap」「Mapped File」中的哪一个,会极大地影响后续排查的成本。17

11.5. 修复后应在相同条件下比较变化趋势

仅仅比较修复前后的峰值是不够的。如果初始值不同,结论很容易反转。

  • 相同的启动状态
  • 相同的输入
  • 相同的操作次数
  • 相同的等待时间
  • 相同的采样间隔

在这些条件下,比较各个周期结束后的底值与变化趋势。证明泄漏已被修复,靠的不是「最大值变小了」,而是即使反复施加相同负载,增长也能够收敛

12. 把常见的误解重新表述一遍

误解 1:任务管理器的「内存」=应用分配的全部内存量

更准确的表述: 应先确认是哪一列。如果是 Working Set 系列,指的是当前常驻 RAM 的量;如果是 Commit Size 系列,指的是进程固有的 Commit 量。

误解 2:Private Bytes =页面文件上的字节数

更准确的表述: Private Bytes 是 Private 的 Commit Charge。它是一个逻辑上的承诺量,既包含位于 RAM 中的页面,也包含需要时可由页面文件支撑的页面。

误解 3:Commit X/Y =页面文件使用量/页面文件容量

更准确的表述: X 是系统整体的 Commit Charge,Y 是 Commit Limit。页面文件会扩展 Y,但 X 并不会原样等同于磁盘上的使用量。

误解 4:Page Faults/sec 高 =正在向磁盘换出(swap)

更准确的表述: 其中也包含软错误。是否伴随磁盘 I/O,需要通过 Pages Input/sec、Page Reads/sec 以及磁盘延迟来确认。

误解 5:Free RAM 少 =内存不足

更准确的表述: 应该看 Available、Standby、硬分页、响应时间。用可复用的缓存把 RAM 填满,是正常现象。

误解 6:能把 Working Set 缩小 =修复了内存泄漏

更准确的表述: 也许只是把页面从 RAM 中赶出去了而已。要确认 Private Bytes 或堆内持有的内存是否真的减少了。

误解 7:Private Bytes 增长了 =确定存在泄漏

更准确的表述: 只有确认了反复执行相同工作负载时是否会收敛、增长的是哪种类型的内存、是否属于可释放的缓存,才能做出判断。

13. 总结

  • Windows 的「内存使用量」并不是一个单一的数字。应该把地址空间、Commit、RAM 常驻、可共享性分开来考虑。
  • Working Set 是当前位于 RAM 中的页面,同时包含 Private 与 Shared 两部分。Private Working Set 是其中进程固有的常驻页面。
  • Private Bytes 是进程固有的 Commit Charge,既不是当前位于 RAM 中的量,也不是实际写入页面文件的量。
  • Committed X/Y 是系统整体的 Commit Charge / Commit Limit。页面文件主要支撑 Commit Limit、已修改页面的换出,以及崩溃转储。
  • Reserve 的虚拟地址、已 Commit 的页面、实际被访问并进入 Working Set 的页面,是三个不同的阶段。
  • Page Fault 是正常操作,软错误不会读取磁盘。硬错误也不仅来自页面文件,还会来自 EXE、DLL、映射文件。
  • 内存泄漏不应根据某一时刻的大小来证明,而应根据相同负载之后的底值与变化趋势,以及具体构成来证明。
  • 基本原则是:单个进程的构成用 VMMap,系统整体的物理 RAM 用 RAMMap,时间序列用 PerfMon,运行时内部则使用专用的转储工具。

下次在任务管理器中注意到「内存在增长」时,请首先这样反问自己:

增长的到底是 Working Set,还是 Private Bytes,还是 Virtual Bytes,还是 System Commit?

仅凭这一个问题,排查的切入点就会精准得多。

相关文章

相关咨询领域

合同会社小村软件针对 Windows 应用的内存增长、长期运行后的性能劣化、32 位进程的 OutOfMemory、以及仅在客户环境中出现的内存不足问题,提供结合 PerfMon・VMMap・RAMMap・WinDbg・.NET 诊断工具的原因调查服务。不会止步于笼统的「内存偏多」,而是深入切分到具体是哪个区域、因何种操作、为何增长,以及从何处被引用・保留。

参考链接

  1. Microsoft Learn, Working Set. 关于进程的 Working Set 是当前常驻于物理内存的页面集合、包含共享页面、软・硬页面错误的区别、Transition 页面,以及从 Working Set 中移除页面等内容。  2 3 4 5

  2. Microsoft Learn, PROCESS_MEMORY_COUNTERS_EX2 structure. 关于 WorkingSetSize、PrivateWorkingSetSize、PrivateUsage、SharedCommitUsage 的定义,以及 PagefileUsage 与 PrivateUsage 都表示进程 Commit Charge 这一内容。  2 3 4

  3. Microsoft Learn, Introduction to page files. 关于页面文件支撑已修改页面的换出、系统崩溃转储、System Commit Limit 的扩展,System Commit Charge 与 Commit Limit 的定义,以及在任务管理器和性能计数器中的测量方式。  2 3 4

  4. Microsoft Learn, Page State. 关于虚拟页面的 Free、Reserved、Committed 三种状态,以及 Reserved 页面尚未关联物理存储、无法访问这一点。  2

  5. Microsoft Learn, VirtualAlloc function. 关于 MEM_RESERVE 与 MEM_COMMIT 的区别、Commit 时会从系统整体的内存与页面文件中计入 Charge,以及实际的物理页面有可能要到首次访问时才会被分配。  2 3

  6. Microsoft Learn, How to determine the appropriate page file size for 64-bit versions of Windows. 关于页面文件的大小取决于峰值 Commit Charge 与崩溃转储需求、硬页面错误的读取来源并不限于页面文件、还包括 EXE・DLL・内存映射文件,以及相关的性能计数器。  2 3 4 5 6

  7. Microsoft Learn, Virtual Address Space. 关于每个进程都拥有独立的虚拟地址空间与页表,以及虚拟地址本身并不等于物理地址。 

  8. Microsoft Learn, Memory Limits for Windows and Windows Server Releases. 关于 32 位进程的用户模式虚拟地址空间通常为 2GB,在 64 位 Windows 上会因 IMAGE_FILE_LARGE_ADDRESS_AWARE 是否启用而变为 2GB 或 4GB。 

  9. Microsoft Learn, SetProcessWorkingSetSize function. 关于 Working Set 的最小・最大值并不保证常驻、可以清空 Working Set,以及过度的设置或操作可能会使系统性能恶化。 

  10. Microsoft Learn, Memory Performance Information. 关于 Windows 的性能计数器、内存管理 API、任务管理器显示三者之间的对应关系,以及 Process 的 Working Set・Working Set - Private・Private Bytes,System 的 Committed Bytes・Commit Limit。 

  11. Microsoft Learn, MapViewOfFile function. 关于 FILE_MAP_COPY 会使所有页面都可能变为 Copy-on-write,因此 Windows 会在映射的那一刻就为整个视图预留可由页面文件支撑的 Commit Charge。 

  12. Microsoft Learn, Understanding Node Metrics and Properties in HPC Cluster Manager. 关于 Available Physical Memory 是 Zeroed、Free、Standby 三种列表之和,以及各页面列表的含义。 

  13. Microsoft Sysinternals, RAMMap. 关于该工具按用途、页面列表、进程、优先级、物理页面、文件等维度分析 Windows 物理内存使用情况的功能。  2

  14. Microsoft Learn, Process.WorkingSet64 Property. 关于 WorkingSet64 以字节为单位返回进程的 Working Set,并对应 Process 的 Working Set 性能计数器。 

  15. Microsoft Learn, Process.PrivateMemorySize64 Property. 关于 PrivateMemorySize64 返回无法与其他进程共享的进程固有内存,并对应 Private Bytes 性能计数器。 

  16. Microsoft Learn, Process.VirtualMemorySize64 Property. 关于 VirtualMemorySize64 返回进程的虚拟内存量,并对应 Virtual Bytes 性能计数器。 

  17. Microsoft Sysinternals, VMMap. 关于该工具按类型分解进程已 Commit 的虚拟内存、显示各类型所分配到的物理内存(Working Set)与详细的内存映射等功能。  2

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。

本文与以下服务页面相关联,欢迎从最接近的入口查看。

常见问题

汇总了咨询这一主题时常见的问题。

任务管理器中的「内存」,是应用所分配的全部内存吗?
不是。任务管理器中存在Working Set系列、Private Working Set系列、Commit Size等多个内存相关列,查看哪个界面・哪一列,含义都不相同。Working Set是当前载入RAM的页面,Private Bytes或Commit Size则是该进程固有的Commit量。请不要把某一个「内存」列,直接理解为应用分配的全部容量或泄漏量。
Working Set与Private Bytes有什么区别?
Working Set是该进程可见、且当前常驻于物理RAM的页面量,其中包含DLL、内存映射文件等共享页面。Private Bytes是仅由该进程使用的已Commit内存量,与当前是否常驻RAM无关。因此二者不会是同一个值,也不存在单纯的大小关系。
任务管理器中的「已提交 18/32GB」,是指已经向页面文件写入了18GB吗?
不是。左边的数字是系统整体当前承诺的Commit量,右边的数字是系统所能支撑的Commit上限。上限大致由RAM与页面文件的总和决定,但左边的全部数量并不都位于页面文件上。大多数已Commit的页面位于RAM中,也有一些页面从未被分配过物理页面。另一方面,像EXE・DLL・内存映射文件这类可以从原始文件重新读取的页面,增加Working Set未必会让Private的Commit增加同等的数量。
明明有空闲RAM,也会发生OutOfMemory吗?
会的。因为除了物理RAM之外,还存在其他会导致分配失败的条件,例如32位进程的虚拟地址空间不足、缺乏连续的空闲地址范围、系统的Commit上限、Job Object或运行时特有的上限等。尤其是运行在64位Windows上的32位进程,如果没有启用Large Address Aware,用户模式虚拟地址空间通常上限为2GB。
禁用页面文件,Windows会变快吗?
一般来说,不能断定就会变快。禁用页面文件会降低系统的Commit上限,使得不再使用的已修改页面更难从RAM中换出,并且还会影响崩溃转储的配置。页面文件的大小,应该在测量峰值Commit量与所需崩溃转储类型之后再决定,而不是没有依据就直接禁用的设置。
Page Faults/sec数值高,就是内存不足吗?
仅凭这一点无法判断。页面错误分为可通过RAM内的Standby页面或与其他进程共享中的页面来解决的软错误,以及需要从磁盘读取的硬错误。请不要只看Page Faults/sec单独一项,而应把Pages Input/sec、Page Reads/sec、Available MBytes、磁盘延迟、处理时间放在同一时间轴上一并确认。

作者简介

本文作者的个人简介页面。

Go Komura

小村软件有限公司 代表

以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表