What Does Windows' "Memory Usage" Actually Mean? — Correctly Reading Working Set, Private Bytes, Commit, and the Page File
· Go Komura · Windows, Windows Development, Memory Management, Working Set, Private Bytes, Commit, Page File, Performance Monitoring, Troubleshooting, Sysinternals
Task Manager shows a process’s “Memory” as 1.2GB. Yet Process Explorer shows a Working Set of 1.5GB and Private Bytes of 2.4GB, and VMMap’s Size is larger still. Looking at the system as a whole, it reads “Committed 19.6/31.8GB”.
So how many gigabytes of memory is this app actually using, in the end?
The answer is that which number you should look at depends on what you actually want to know. The metric to use differs depending on whether you want to know the amount currently resident in RAM, the amount allocated specifically to that process, the amount the system has promised to keep backing in the future, or simply the range of virtual addresses that have been reserved.
What makes Windows memory metrics confusing is that they are all displayed under the same word, “memory”, even though they actually measure the following separate axes.
- How much address space is in use
- How much commit has been consumed
- Whether it is currently resident in physical RAM
- Whether the page is private to the process, or shareable
- How much more allocation the system as a whole can still support
This article is aimed at anyone investigating growing app memory or system-wide memory shortages on Windows 10/11 and current Windows Server, and ties together, in a single picture, the relationships between Working Set, Private Working Set, Private Bytes, Commit, Virtual Bytes, the page file, Available, and page faults.
The procedure for tracking down why .NET objects are not being collected is covered in detail in “Telling GC Lag from a Memory Leak in .NET”, and the concrete operation of VMMap and Process Explorer is covered in “Process Explorer / Handle / VMMap in Practice”. This article focuses on the prerequisite for both: how to read the numbers on the Windows OS side.
1. The Bottom Line First
- Working Set is the set of pages currently resident in RAM. It includes not only pages private to the process but also pages that can be shared with other processes, such as DLL code and memory-mapped files.1
- Private Working Set is the portion of the Working Set that currently belongs only to that process. It is useful as an approximation of “the RAM this process alone currently occupies”, but it is not the total amount the app has allocated.2
- Private Bytes is the amount of commit private to that process. It is a separate metric from whether the memory is currently resident in RAM. The
PagefileUsagefield in the Win32 API structure also, on current Windows, effectively represents the same Commit Charge, and is not the number of bytes actually written to the page file.2 - Task Manager’s “Committed X/Y” shows X as the system’s current total commit and Y as the commit ceiling. X is not page file usage. Y is determined roughly by RAM plus the page file.3
- Reserve and Commit are different things. Merely Reserving a virtual address range only sets that range aside for future use; it does not consume the same amount of either RAM or the commit ceiling.45
- A page fault does not necessarily mean disk I/O. There are soft faults, which can be resolved within RAM, and hard faults, which read from the page file, executable files, memory-mapped files, and the like.16
- A memory leak is judged not from a single reading but from the trend when the same load is repeated. In particular, watch whether Private Bytes and its breakdown keep creeping up step by step even after processing ends, without returning to the same steady state.
In one sentence: Working Set is “the amount currently in RAM”, Private Bytes is “the amount promised specifically to this process”, and Commit is “the amount the system as a whole has promised”.
flowchart TB
accTitle: Choosing the right Windows memory metric
accDescr: Which metric to look at depends on whether you want to know RAM residency, process-private commit, system-wide commit, or virtual address range
question["What do you want to know about memory usage"]
question -->|amount currently in RAM| workingSet["Working Set"]
question -->|process-private promised amount| privateBytes["Private Bytes"]
question -->|system-wide promised amount| systemCommit["System Commit"]
question -->|reserved address range| virtualBytes["Virtual Bytes / Reserved"]
workingSet --> resident["Residency in physical RAM"]
privateBytes --> privateCommit["Process-private Commit"]
systemCommit --> commitLimit["Compare against Commit Limit"]
virtualBytes --> addressSpace["Virtual address space"]
Figure 1: Break the observation “memory is high” down into four separate questions first.
2. Splitting “Memory Usage” Into Four Axes
To start, think of Windows memory not as “a single bar” but along four axes.
flowchart TB
accTitle: Four independent axes for classifying a single page
accDescr: Check the virtual address state, the backing of committed pages, residency in physical RAM, and shareability with other processes separately
page["Look at one page along four axes"]
page --> address["Address state"]
address --> addressValues["Free / Reserved / Committed"]
page --> backing["Backing"]
backing --> backingValues["Page-file-backed / File-backed"]
page --> residentAxis["RAM residency"]
residentAxis --> residentValues["Resident / Not resident"]
page --> sharing["Shareability"]
sharing --> sharingValues["Private / Shareable"]
Figure 2: Even for a single page, address state, backing, residency, and shareability are each determined independently.
Mapped is not an address state alongside Free, Reserved, and Committed — it is a category of region. Pages in a mapped view can also be Committed. Likewise, Private is not a backing medium but a classification of shareability. So read backing as either Page-file-backed or File-backed, and shareability as either Private or Shareable, separately.
Combining these four axes gives the relationship between the representative metrics as follows.
| Page state | Working Set | Private Working Set | Private Bytes | Virtual Bytes family |
|---|---|---|---|---|
| Process-private, committed, RAM-resident | Included | Included | Included | Included |
| Process-private, committed, not RAM-resident | Not included | Not included | Included | Included |
| Shared page of a DLL or mapped file, RAM-resident | Included | Generally not included | Generally not included | Included |
| Reserved but not committed | Not included | Not included | Not included | May be included |
| Unused address range | Not included | Not included | Not included | Usually not included |
flowchart TB
accTitle: Mapping page types to the main memory metrics
accDescr: Shows which metrics include resident private pages, non-resident private pages, resident shared pages, and reserved-only ranges
privateResident["Private, committed, RAM-resident"]
privateNonresident["Private, committed, not RAM-resident"]
sharedResident["Shared page, RAM-resident"]
reservedOnly["Reserved, not committed"]
workingSet["Working Set"]
privateWorkingSet["Private Working Set"]
privateBytes["Private Bytes"]
virtualBytes["Virtual Bytes family"]
privateResident --> workingSet
privateResident --> privateWorkingSet
privateResident --> privateBytes
privateResident --> virtualBytes
privateNonresident --> privateBytes
privateNonresident --> virtualBytes
sharedResident --> workingSet
sharedResident --> virtualBytes
reservedOnly --> virtualBytes
Figure 3: Working Set and Private Bytes count different sets of pages, so they are not in a simple containment relationship.
The important point here is that Working Set and Private Bytes are not in a simple containment relationship.
Private Bytes includes pages that are private to the process but not currently resident in RAM. Working Set, on the other hand, includes shared pages — such as DLL code and shared memory — that Private Bytes does not count at all. So depending on the process and the moment in time, Working Set can be larger than Private Bytes, or the reverse can be true.
Also, simply summing the Working Sets of multiple processes can count the same physical page — such as a shared DLL — more than once. “The sum of each process’s Working Set equals the RAM in use” does not necessarily hold.
3. Virtual Address Space — Reserve and Commit Are Different Things
3.1. A Virtual Address Is Not a Physical RAM Address
Each process has its own private virtual address space. A pointer an app works with does not directly indicate a location in physical RAM; Windows uses page tables to map virtual addresses to physical pages or to data on a file.7
As a result, even on a PC with 64GB of RAM installed, the virtual address space a given 32-bit process can use is normally far smaller than that. Conversely, it is also normal for a 64-bit process to have a virtual address space larger than physical RAM.
3.2. Reserved Only Means “the Address Has Been Staked Out”
VirtualAlloc’s MEM_RESERVE reserves a contiguous virtual address range for future use. At this stage no physical storage is associated with the pages, and the range cannot be read or written.45
For example, even if a database or runtime Reserves an 8GB address range for future growth, that alone does not consume 8GB of RAM or 8GB of Private Bytes.
3.3. Committed Is a Promise to “Back It When It’s Needed”
MEM_COMMIT is the operation that puts a virtual page into the Committed state and has Windows promise to provide the necessary backing. Whether reading, writing, or execution is actually permitted is decided separately by the page protection — PAGE_READONLY, PAGE_READWRITE, PAGE_EXECUTE, PAGE_NOACCESS, and so on — so being Committed by itself does not mean “readable and writable”. The moment it is committed it is counted toward the system’s Commit Charge, but the actual physical page may not be assigned until the first access. A page touched for the first time is zero-initialised, goes through a demand-zero fault, and enters the Working Set.51
So even though we call it “allocated” either way, there are actually the following three stages.
flowchart TB
accTitle: Three stages from Reserve through Commit to RAM residency
accDescr: Shows the flow of reserving a virtual address, committing the page, and having the first access assign a physical page and enter the Working Set
reserve["MEM_RESERVE - reserve an address range"]
reserve -.-> virtualMetric["Reflected in the Virtual Bytes family"]
reserve -->|MEM_COMMIT| committed["Committed - accessible according to page protection"]
committed -.-> commitMetric["Reflected in Private Bytes / System Commit"]
committed -->|first access, demand-zero fault| resident["Physical page assigned, RAM-resident"]
resident -.-> workingSetMetric["Reflected in Working Set"]
committed -.->|if never accessed| nonresident["Committed but not resident"]
Figure 4: Reserve, Commit, and the first access are separate events, and each moves a different metric.
These three stages move the numbers for the Virtual Bytes family, Private Bytes, and Working Set separately, respectively.
3.4. Why You Can Get OutOfMemory Even With Free RAM Available
Whether a memory allocation succeeds is not determined by free RAM alone.
- The process has exhausted its virtual address space
- There is no free address range of the required size that is contiguous
- The system-wide Commit Charge has reached the Commit Limit
- A Job Object, container, runtime, or library has its own limit
- It is a 32-bit process
- The native heap is fragmented
Even on 64-bit Windows, a 32-bit process’s user-mode virtual address space is normally 2GB if IMAGE_FILE_LARGE_ADDRESS_AWARE is not set. A 32-bit app with that flag set can use up to 4GB on 64-bit Windows.8
So “the PC has 20GB of free RAM, yet the 32-bit app fails at around 1.6GB” is not a contradiction. It may not be a RAM problem at all, but rather address space fragmentation or a hard limit being hit.
4. Working Set — Pages Currently in RAM
Working Set is the set of pages, within a process’s virtual address space, that are currently resident in physical RAM.1
This set is a mixture of the following.
- The process’s own heap and stack
- EXE and DLL code and read-only data
- Memory-mapped files
- Shared memory
- Pages that became private to that process after copy-on-write
- Pages touched by the runtime and various libraries
4.1. A Growing Working Set Does Not Necessarily Mean More Was Allocated
Accessing a page that was already committed for the first time can increase the Working Set alone while Private Bytes stays unchanged. Likewise, when a large file is memory-mapped and read sequentially, file-backed pages enter the Working Set while Private Bytes barely increases.
Conversely, when Windows Trims the Working Set in response to memory pressure, the Working Set alone shrinks while the app logically still holds the same memory. Touching it again later brings it back through a page fault.
So a drop in Working Set does not necessarily mean “the app freed it”, and a rise does not necessarily mean “the app newly allocated it”.
flowchart TB
accTitle: A typical flow where only the Working Set rises and falls
accDescr: The same committed page enters RAM on first access, becomes non-resident on Trim, and returns on re-access, while Private Bytes keeps being counted throughout
committed["The same committed page"]
committed -->|first access| resident["RAM-resident"]
resident -->|Trim under memory pressure| nonresident["Not resident"]
nonresident -->|page fault on re-access| resident
resident -.-> inWorkingSet["Included in Working Set"]
nonresident -.-> outsideWorkingSet["Not included in Working Set"]
committed -.-> privateBytes["Counted in Private Bytes while committed"]
Figure 5: Working Set rises and falls with residency, but Private Bytes does not decrease as long as the commit on the same page remains.
4.2. Working Set Includes Shared Pages
If 10 processes share the same DLL’s code pages, that page can show up in each process’s Working Set, even though only one copy exists in physical RAM. The sum of Working Sets exceeding installed RAM is not immediately a sign of trouble.
If you want to get closer to “the RAM this process alone currently occupies”, look at Private Working Set. Even so, this is not “all the memory that process has allocated” either — it is strictly the private pages currently resident.
4.3. Forcing the Working Set Down Does Not Fix a Leak
You can use EmptyWorkingSet or SetProcessWorkingSetSize to evict pages from a process’s Working Set. But this is not an operation that frees commit or releases references on the heap. The apparent RAM usage drops while Private Bytes stays unchanged, and the next access can trigger a burst of page faults.9
If Task Manager’s figure shrinks only right after you press a “reduce memory” button, and immediately climbs back once you resume working, it may just be a Working Set Trim rather than an actual “release”.
5. Private Bytes — the Commit Amount Private to a Process
Private Bytes is the amount of virtual memory committed exclusively for that process. It represents Commit Charge that cannot be shared with another process, and it does not matter whether it is currently resident in RAM. In Microsoft’s PROCESS_MEMORY_COUNTERS_EX, PrivateUsage corresponds to this value.102
The Win32 API also has a confusingly named field, PagefileUsage, but current documentation defines it as “that process’s Commit Charge” and states it is the same value as PrivateUsage. In other words, Private Bytes of 2GB does not mean “2GB has been written to pagefile.sys”.2
Private Bytes is typically affected by the following.
- Commit of the native heap used by
HeapAlloc,malloc,new, and similar - Private Data committed directly with
VirtualAlloc - The committed region of the .NET GC heap
- The portion of a thread stack that has actually been committed
- The Commit Charge for the entire view reserved when a copy-on-write view (
FILE_MAP_COPY) is mapped - Private buffers held internally by libraries and device SDKs
In a copy-on-write view created with FILE_MAP_COPY, each page could eventually become private, so at mapping time Windows reserves enough Commit Charge to back the entire view with the page file. Because of this, System Commit and the process’s Commit Charge (Private Bytes) can rise by the size of the whole view even before any write actually creates a private copy.11
5.1. Why Private Bytes Does Not Drop After free or a GC
Even when memory is “freed” from the application’s point of view, the runtime or heap allocator may not Decommit that region back to the OS, and instead retain it for future reuse. In that case, Private Bytes stays high even though the region is reusable internally within the app.
It can also stay high for reasons such as only part of a large region still being alive, fragmentation, or a cache or pool having warmed up to its ceiling.
So high Private Bytes alone does not prove a leak. What you should look at is a comparison over time:
- Repeat the same processing the same number of times
- Wait the same amount of time after processing
- Check whether Private Bytes returns to the same level, or plateaus at a fixed value
- Use VMMap or a heap dump to check which region or type grew
flowchart TB
accTitle: Why Private Bytes does not drop after free or a GC
accDescr: Private Bytes changes differently depending on whether the allocator returns to the OS a region the app no longer needs, or retains it for reuse
release["App frees a region via free / GC"]
release --> decision{"Does the allocator return it to the OS"}
decision -->|Decommit / Release| returned["Commit Charge decreases"]
returned --> lower["Private Bytes drops"]
decision -->|retains for reuse| retained["Region stays committed"]
retained --> high["Private Bytes plateaus high"]
retained --> reasons["Pools, caches, fragmentation"]
Figure 6: A region becoming reusable within the app is not the same as its Commit being returned to the OS.
5.2. A Strong Candidate Pattern for a Leak
An increase like the following, where the floor ratchets up with each round of load in a “staircase” pattern, deserves attention.
Private Bytes
^
| ________
| ______|
| ______|
|_____|
+----------------------------> Repetitions of the same processing
That said, even a staircase shape can just be a few rounds of growth from first-time JIT compilation, fonts, image decoders, connection pools, or cache warm-up, after which it stabilises. What matters is not that it is increasing, but that it fails to converge to a steady state.
6. System Commit — What “Committed X/Y” Actually Is
The “Committed X/Y” figure on Task Manager’s [Performance] → [Memory] tab is a system-wide metric.
- X: System Commit Charge — the committed memory that Windows is currently promising to back across the whole system
- Y: System Commit Limit — the ceiling of commit that the system can support
Commit Limit is determined roughly by physical RAM plus the total of all page files. Without a page file, it comes out a little smaller than installed RAM.36
flowchart TB
accTitle: The relationship between System Commit Charge and Commit Limit
accDescr: Per-process, shared-section, and kernel commit make up the current value X, while physical RAM and the page file support the ceiling Y
processCommit["Each process's Private Commit"] --> charge["System Commit Charge - X"]
sharedCommit["Commit of shared sections backed by the page file"] --> charge
kernelCommit["Kernel Commit"] --> charge
physicalRam["Physical RAM"] --> limit["System Commit Limit - Y"]
pageFiles["Page file"] --> limit
charge -->|X cannot exceed Y| limit
Figure 7: X is the current promised amount and Y is the ceiling that can support that promise — this is not a display of page file usage.
System Commit Charge includes not only the sum of each process’s Private Bytes, but also the Commit of page-file-backed shared sections and the Commit consumed by the kernel. So the sum of per-process Private Bytes alone cannot fully account for X.
6.1. Commit Charge Is Not Page File Usage
Consider a system with 16GB of RAM, a 16GB page file, and Committed at 20/31GB.
That 20GB does not mean “20GB has been written to the page file”. It is the total amount that Windows is promising to provide RAM or page-file backing for, whenever needed, for private writable pages and the like.
At that moment, a mix of the following states can hold:
- Most of it is resident in RAM
- Some of it has been paged out to the page file
- Some is committed but has not yet had its first access
- Some is consumed as commit on the kernel side
If you want to see actual page file usage, check Paging File(*)\% Usage separately from Commit. Even Microsoft’s own material explains that high page file usage alone does not necessarily indicate a performance problem, and that it should be judged together with reaching the Commit Limit, the Modified Page List, and actual paging I/O.6
6.2. What Happens as You Approach the Commit Limit
When System Commit Charge reaches the Commit Limit, new commit requests cannot be backed. This leads to process memory allocation failures, app crashes, and the system becoming unresponsive.3
Here, Commit’s X/Y matters more than “free RAM”. Even if you Trim Working Sets to free up RAM, reaching the Commit Limit is not resolved unless the Commit Charge itself decreases.
6.3. The Three Roles of the Page File
The page file mainly serves the following roles.
- Extending the Commit Limit
- Allowing infrequently used modified pages to be paged out of RAM
- Backing the system crash dump, depending on configuration
Disabling the page file is not a simple case of “disk I/O always goes down and things get faster”. If anything, it lowers the Commit Limit, makes it more likely that modified but currently unneeded pages stay in RAM, and can make it impossible to capture the dump you need when a crash occurs.36
The appropriate page file size cannot be decided from installed RAM alone. Microsoft itself explains that this cannot be generalised, because peak System Commit Charge and the kind of crash dump required differ from system to system.6
7. The Breakdown of Physical RAM — Don’t Judge from Low Available Alone
Physical RAM is not used solely by user processes’ Working Sets.
- Each process’s Working Set
- The system file cache
- Page lists such as Standby, Modified, Free, and Zeroed
- The kernel’s Paged Pool / Nonpaged Pool
- Memory held by device drivers
- The memory compression store
- Regions shared with or reserved for the GPU and other devices
- Hardware-reserved memory
7.1. Available Includes Reusable Cache Too
Windows’ Available MBytes is not simply completely unused RAM. It is a metric that, alongside Free and Zeroed, also includes Standby pages that can be reused if needed.12
- Free: pages not currently allocated to any purpose
- Zeroed: pages that have been zeroed out so they can be safely handed to another process
- Standby: pages that have left a Working Set but whose contents are still cached in RAM
- Modified: pages whose contents have changed and that need to be written back to appropriate backing storage before reuse
flowchart TB
accTitle: Movement between the Working Set and the page lists
accDescr: Shows unchanged pages leaving to Standby and modified pages leaving to Modified, then re-access, write-back, and reuse
workingSet["Working Set - in use"]
workingSet -->|unchanged page removed| standby["Standby - reuse candidate with content retained"]
workingSet -->|modified page removed| modified["Modified - awaiting write-back"]
modified -->|write-back complete| standby
standby -->|re-access| workingSet
standby -->|reused for another purpose| reused["Allocated to another purpose"]
free["Free - unused"] -->|zeroed| zeroed["Zeroed - available for new allocation"]
zeroed -->|accessed after allocation| workingSet
standby -.-> available["Included in Available"]
free -.-> available
zeroed -.-> available
Figure 8: Available includes not only fully free memory but also Standby, which can be reused if needed.
“Discarding all cache to increase free RAM” is not always a win. If the data you need is still sitting in Standby, re-accessing it can bring it back into the Working Set quickly without reading from disk.
So even if Free is low in Task Manager, if Available is ample and hard page faults or disk waits are not causing problems, Windows may simply be making effective use of RAM as cache.
7.2. When RAM Shrinks Without Any Large Process
It is not unusual for memory consumption to be unexplained even after summing every process’s Private Working Set.
- File cache and memory-mapped files
- Nonpaged Pool / Paged Pool
- Pages locked by a driver
- Shared pages
- Memory compression
- Allocations related to virtualisation or the GPU
In this case, rather than continuing to stare at the process list, check Use Counts, Processes, Priority Summary, and File Summary in Sysinternals’ RAMMap. RAMMap is the official tool for breaking physical memory down by purpose, page list, and file.13
If only Nonpaged Pool keeps growing, that is the point to suspect a leak on the driver or kernel side, rather than in a user-mode app’s Private Bytes.
8. Page Faults — A High Count Is Not Abnormal by Itself
A Page Fault occurs when a process accesses a page that is not currently in its Working Set. Despite the word “Fault” in the name, this is not an exceptional failure — it is the normal mechanism that drives virtual memory.1
8.1. Soft Page Faults
These are resolved without reading from disk.
- The page is still in Standby or Transition
- The same shared page is already in another process’s Working Set
- A committed page is accessed for the first time and a zero page is assigned
- The memory manager’s read-ahead has already brought it into RAM
For this reason, a large \Memory\Page Faults/sec does not necessarily mean disk I/O or latency is occurring.
8.2. Hard Page Faults
These require reading content from a Backing Store on disk. The source is not limited to the page file.
- Code and data in an
.exeor.dll - A memory-mapped file
- The page file
flowchart TB
accTitle: The branch between soft and hard page faults
accDescr: When accessing a page not in the Working Set, it is handled as a soft page fault if storage I/O is unnecessary, or a hard page fault if it is necessary
access["Access a page not in the Working Set"] --> storageIo{"Is storage I/O required"}
storageIo -->|No - Standby, shared, demand-zero, etc| soft["Soft page fault"]
soft --> resident["Enters Working Set without reading disk"]
storageIo -->|Yes| hard["Hard page fault"]
hard --> source{"Where is it read from"}
source --> image["EXE / DLL"]
source --> mapped["Memory-mapped file"]
source --> pagefile["Page file"]
image --> loaded["Enters Working Set after loading"]
mapped --> loaded
pagefile --> loaded
Figure 9: The name “Page Fault” alone cannot tell you whether disk I/O occurred.
Microsoft lists \Memory\Pages/sec, \Memory\Page Reads/sec, and \Memory\Pages Input/sec among the counters for measuring hard faults. Since these being high does not necessarily mean memory is low, correlate them with Available MBytes, disk latency, and actual response time.6
8.3. Don’t Set a Single Blanket Threshold
A fixed value such as “anything above 1000 Page Faults/sec is abnormal” changes meaning depending on storage, page size, workload, and access locality.
In practice, line up the following on the same timeline.
Memory\Available MBytesMemory\Pages Input/secMemory\Page Reads/sec- Read latency / Queue on the target disk
- The target process’s Working Set and Private Bytes
- The app’s processing time, timeouts, and UI responsiveness
If Available drops at the same time as load rises, Pages Input/sec and disk wait go up, and processing time also worsens, that gives you grounds to suspect paging caused by physical memory pressure.
9. Which Screen or Tool to Check for What
| What you want to know | The metric to check first | Main tools |
|---|---|---|
| The amount the target process currently has in RAM | Working Set | Task Manager, Process Explorer, Get-Process |
| The private portion of that — RAM private to the process | Private Working Set / Working Set - Private | Task Manager’s Details columns, Process Explorer, PerfMon |
| The commit amount private to the target process | Private Bytes / Commit Size | Process Explorer, PerfMon, VMMap, Get-Process |
| The process’s virtual address range | Virtual Bytes / Size | Process Explorer, VMMap, Get-Process |
| The system’s overall commit headroom | Committed Bytes / Commit Limit | Task Manager [Performance], PerfMon |
| Physical RAM’s reuse headroom | Available MBytes | Task Manager, PerfMon |
| The breakdown of Standby, Modified, and file cache | Page list / purpose breakdown | RAMMap |
| What grew within Private Bytes | Heap / Private Data / Managed Heap, etc. | VMMap, WinDbg, runtime-specific dumps |
| Paging involving disk | Pages Input/sec, Page Reads/sec, disk latency | PerfMon, WPR/WPA |
flowchart TB
accTitle: Choosing a Windows memory investigation tool
accDescr: The tool to use depends on whether the target is one process or the whole system, a single point in time or a time series, and whether you need to trace retention inside a runtime
question["What do you want to isolate"]
question --> processScope{"Is the target a single process"}
processScope -->|yes| processTime{"A single point in time or a time series"}
processTime -->|single-point breakdown| vmmap["VMMap"]
processTime -->|time series| perfmon["PerfMon / PowerShell"]
processScope -->|whole system| systemView{"Physical RAM breakdown or a timeline"}
systemView -->|physical RAM breakdown| rammap["RAMMap"]
systemView -->|timeline including CPU, I/O, and waits| wpa["WPR / WPA"]
question --> runtime{"Do you need to trace retention inside a runtime"}
runtime -->|.NET heap| dotnet["dotnet-dump / PerfView"]
runtime -->|native heap| native["WinDbg / Application Verifier"]
Figure 10: Deciding the scope and timeline first lets you pick exactly the tool you need, no more and no less.
9.1. Task Manager
In Task Manager, look at the screens separately.
- [Processes] or [Details]: individual processes’ Working Set family and Commit Size family
- [Performance] → [Memory]: system-wide In use, Available, Committed, Cached, Paged pool, Non-paged pool
Don’t judge from the column named “Memory” alone — right-click the column headers on the [Details] tab and add the columns you need, such as Working Set, Peak Working Set, and Commit Size. Column names vary somewhat by Windows version and display language, so confirm what a column actually means before you record it.
9.2. Capturing a Time Series With PowerShell
If you know the target process’s ID, you can capture the trends of Working Set, Private Bytes, and Virtual Bytes together with Get-Process.
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
.NET’s Process.WorkingSet64 corresponds to Working Set, PrivateMemorySize64 to Private Bytes, and VirtualMemorySize64 to Virtual Bytes.141516
For an app with multiple instances, track by PID rather than by name. For long-term monitoring where a restart changes the PID, design the collection to record the start time, service name, and similar, so the target is never mistaken.
9.3. Putting the System and a Process on the Same Timeline With PerfMon
Recording at least the following together makes isolation much easier.
\Process(<target>)\ID Process
\Process(<target>)\Working Set
\Process(<target>)\Working Set - Private
\Process(<target>)\Private Bytes
\Process(<target>)\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
When multiple processes share the same name, or a restart happens during monitoring, the instance name alone — Process(name) or Process(name#N) — cannot pin down the target. Record ID Process for each sample too, and adopt only the instance whose value matches the PID you are tracking. When you span a restart that changes the PID, record the time of the switch separately as well.
Windows performance counter names can be localised depending on the display language. If specifying the English name directly in PowerShell fails to find it, add the counter through PerfMon’s GUI, or check the names in your local environment with Get-Counter -ListSet *.
9.4. Don’t Confuse the Roles of VMMap and RAMMap
- VMMap: breaks down one process’s virtual memory and Working Set into Heap, Image, Mapped File, Private Data, Managed Heap, and the like
- RAMMap: breaks down the whole system’s physical RAM by purpose, page list, process, and file
“What made this process’s Private Bytes grow” is a job for VMMap; “what is the RAM that the process list cannot explain being used for” is a job for RAMMap.1713
10. Reading Symptoms From Combinations of Numbers
| Observed pattern | First hypothesis | What to check next |
|---|---|---|
| Working Set rises, Private Bytes stable | First access to existing pages, a shared DLL, a mapped file, file cache | VMMap’s Image / Mapped File, Pages Input/sec |
| Private Bytes rises, Working Set stable | Private Commit grew but is non-resident or has been Trimmed | VMMap’s Heap / Private Data / Managed Heap |
| Both rise right after startup, then flatten | JIT, cache, pool, initialisation warm-up | Whether it grows again under the same additional load |
| Private Bytes floor rises with each round of load | A leak, an unbounded cache, or an allocator that retains memory after release | VMMap snapshots before and after, a heap dump |
| Only Working Set suddenly drops and comes back with activity | The OS or the app Trimmed the Working Set | Private Bytes, Pages Input/sec, response time |
| X in Committed X/Y approaches Y | System-wide commit pressure | Top Private Bytes consumers, Paged/Nonpaged Pool, page file settings |
| Available is low, Pages Input/sec and disk latency are high | Physical RAM pressure and hard paging | Top Working Set consumers, RAMMap, workload correlation |
| RAM usage is high but there is no large process | Cache, shared pages, kernel pools, drivers, compression, etc. | RAMMap, Pool Nonpaged/Paged Bytes |
| Free RAM is available, yet only the 32-bit app fails | A virtual address space ceiling or fragmentation | VMMap’s Free/Reserved, the executable’s LAA setting |
| Private Bytes is high but does not grow with repeated processing | A pool or cache possibly holding a high watermark | Its limit, reuse behaviour, stability after the peak |
The single most important thing about this table is to read it in combination, not from a single value alone.
11. A Practical Procedure for Investigating a Memory Leak
11.1. First Decide the Reproduction Conditions and the Steady Point
“It grows over a few days” alone cannot be compared.
- How much of the post-startup warm-up to include
- What one cycle of operation consists of
- How many seconds to wait after one cycle
- How many rounds it takes to reach the cache ceiling
- Whether the same input can be used for the healthy and problematic builds
Decide all of these.
11.2. Record the Process and the System Simultaneously
At minimum, keep the following logged at the same timestamps.
- The target’s Working Set
- The target’s Private Bytes
- The target’s Virtual Bytes
- The system’s Committed Bytes / Commit Limit
- Available MBytes
- Pages Input/sec
- Handle count, thread count
- Number of operations or items processed
If the process’s Private Bytes is stable while the system’s Commit keeps growing, you need to widen your scope to other processes, the kernel, drivers, and shared sections.
11.3. Decide Which “Dimension” Is Growing First
- Working Set alone: resident pages, shared or file-derived, Trim and reload
- Private Bytes: process-private commit
- Virtual Bytes alone: Reserve, mapping, address space fragmentation
- System Commit alone: including other processes and the kernel side
- Nonpaged Pool: driver/kernel side
- Handles / GDI / USER: leaks in resources other than memory
Skip this ordering and jump straight to taking a dump, and you end up reading a mountain of information while targeting the wrong thing.
11.4. Move On to the Breakdown
- Native process: VMMap, WinDbg, Application Verifier, heap tracing
- .NET:
dotnet-counters,dotnet-gcdump,dotnet-dump, PerfView - System-wide: RAMMap, PerfMon, WPR/WPA
- Kernel pool: PoolMon, WinDbg
VMMap displays a process’s committed virtual memory, and the Working Set allocated to each part of it, broken down by type. How far you can narrow the growth in Private Bytes down to Heap, Private Data, Managed Heap, or Mapped File makes a big difference to the cost of the investigation that follows.17
11.5. After a Fix, Compare the Trend Under the Same Conditions
It is not enough for the peak value to differ before and after the fix. If the starting value differs, the comparison can easily flip.
- Same startup state
- Same input
- Same number of operations
- Same wait time
- Same sampling interval
— and compare the floor value and trend after each cycle. Proving a leak fix is not “the maximum got smaller”, but that the growth now converges even when the same load is repeated.
12. Rephrasing Common Misconceptions
Misconception 1: Task Manager’s Memory Equals the Total Amount an App Has Allocated
Rephrased: Check which column it is. For the Working Set family it is the amount currently resident in RAM; for the Commit Size family it is the commit private to that process.
Misconception 2: Private Bytes Equals Bytes on the Page File
Rephrased: Private Bytes is private Commit Charge. It is a logical promised amount that includes both pages currently in RAM and pages that would be backed by the page file if and when needed.
Misconception 3: Commit X/Y Equals Page File Usage / Page File Capacity
Rephrased: X is the system-wide Commit Charge, and Y is the Commit Limit. The page file extends Y, but X does not translate directly into usage on disk.
Misconception 4: A High Page Faults/sec Means It Is Swapping to Disk
Rephrased: This includes soft faults too. Check Pages Input/sec, Page Reads/sec, and disk latency to see whether disk I/O is actually involved.
Misconception 5: Low Free RAM Means Memory Is Short
Rephrased: Look at Available, Standby, hard paging, and response time. Filling RAM with reusable cache is normal.
Misconception 6: Shrinking the Working Set Means a Memory Leak Was Fixed
Rephrased: You may have only evicted pages from RAM. Check whether Private Bytes and what’s retained inside the heap actually decreased.
Misconception 7: An Increase in Private Bytes Confirms a Leak
Rephrased: You can only judge this after checking whether it converges when the same workload is repeated, which kind of memory grew, and whether it is a releasable cache.
13. Summary
- Windows’ “memory usage” is not a single number. Think of address space, commit, RAM residency, and shareability separately.
- Working Set is the pages currently in RAM, including both Private and Shared. Private Working Set is the process-private resident pages within that.
- Private Bytes is process-private Commit Charge; it is neither the amount currently in RAM nor the amount actually written to the page file.
- Committed X/Y is the system-wide Commit Charge / Commit Limit. The page file mainly supports the Commit Limit, the eviction of modified pages, and crash dumps.
- A Reserved virtual address, a Committed page, and a page that has actually been touched and entered the Working Set are separate stages.
- A Page Fault is normal operation, and a soft fault does not read from disk. Hard faults, too, can occur not only from the page file but from an EXE, DLL, or mapped file.
- A memory leak is proven not by its size at a single point in time, but by the floor value and trend after the same load, together with the breakdown.
- The basic path is: VMMap for the breakdown of an individual process, RAMMap for system-wide physical RAM, PerfMon for a time series, and a dedicated dump tool for what’s happening inside a runtime.
Next time you notice in Task Manager that “memory is growing”, start by asking yourself this.
Is what’s growing the Working Set, Private Bytes, Virtual Bytes, or System Commit?
That question alone makes the entry point of your investigation considerably more accurate.
Related Articles
- Telling GC Lag from a Memory Leak in .NET — A Practical Procedure for Observing, Comparing, and Proving Memory Growth
- Process Explorer / Handle / VMMap in Practice — Chasing Hangs, Leaks, and “File in Use” from the State Right Now
- Shared Memory Pitfalls and Practical Best Practices
- The Depths of Windows I/O (Part 4) — Cache Manager: When Does Your WriteFile Actually Reach the Disk?
- Investigating Long-Run Crashes of an Industrial Camera App - The Handle Leak (Part 1)
Related Consulting Areas
KomuraSoft LLC handles root-cause investigations that combine PerfMon, VMMap, RAMMap, WinDbg, and .NET diagnostic tools for Windows app memory growth, performance degradation after long-running operation, OutOfMemory in 32-bit processes, and memory shortages that occur only in a customer’s environment. We do not stop at simply “memory is high” — we isolate which region grew, through which operation, why, and where it is being referenced or retained from.
- Windows Application Development
- Bug Investigation & Root-Cause Analysis
- Technical Consulting & Design Review
- Contact Us
References
-
Microsoft Learn, Working Set. On a process’s Working Set being the set of pages currently resident in physical memory, including shared pages; the difference between soft and hard page faults; Transition pages; and the removal of pages from the Working Set. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, PROCESS_MEMORY_COUNTERS_EX2 structure. On the definitions of WorkingSetSize, PrivateWorkingSetSize, PrivateUsage, and SharedCommitUsage, and on both PagefileUsage and PrivateUsage representing the process’s Commit Charge. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Introduction to page files. On the page file supporting eviction of modified pages, system crash dumps, and extension of the System Commit Limit; the definitions of System Commit Charge and Commit Limit; and how they are measured through Task Manager and performance counters. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Page State. On the Free, Reserved, and Committed states of a virtual page, and on Reserved pages having no physical storage associated with them and being inaccessible. ↩ ↩2
-
Microsoft Learn, VirtualAlloc function. On the difference between MEM_RESERVE and MEM_COMMIT; committing being charged against the system’s overall memory and page file; and the actual physical page sometimes not being allocated until the first access. ↩ ↩2 ↩3
-
Microsoft Learn, How to determine the appropriate page file size for 64-bit versions of Windows. On page file size depending on peak Commit Charge and crash dump requirements; hard page faults being read not only from the page file but also from EXEs, DLLs, and memory-mapped files; and the related performance counters. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, Virtual Address Space. On each process having its own independent virtual address space and page table, and a virtual address not being a physical address itself. ↩
-
Microsoft Learn, Memory Limits for Windows and Windows Server Releases. On a 32-bit process’s user-mode virtual address space normally being 2GB, and becoming either 2GB or 4GB on 64-bit Windows depending on IMAGE_FILE_LARGE_ADDRESS_AWARE. ↩
-
Microsoft Learn, SetProcessWorkingSetSize function. On the Working Set minimum and maximum values not guaranteeing residency; the ability to empty a Working Set; and excessive settings or operations being able to degrade system performance. ↩
-
Microsoft Learn, Memory Performance Information. On the correspondence between Windows performance counters, memory management APIs, and Task Manager’s display, including the Process object’s Working Set / Working Set - Private / Private Bytes, and the System object’s Committed Bytes / Commit Limit. ↩
-
Microsoft Learn, MapViewOfFile function. On
FILE_MAP_COPYmaking every page potentially copy-on-write, so that Commit Charge for the entire view is reserved to be backed by the page file at mapping time. ↩ -
Microsoft Learn, Understanding Node Metrics and Properties in HPC Cluster Manager. On Available Physical Memory being calculated as the sum of the Zeroed, Free, and Standby lists, and on what each of those page lists means. ↩
-
Microsoft Sysinternals, RAMMap. On analysing Windows physical memory usage by purpose, page list, process, priority, physical page, and file. ↩ ↩2
-
Microsoft Learn, Process.WorkingSet64 Property. On
WorkingSet64returning the process’s Working Set in bytes, corresponding to the Process object’s Working Set performance counter. ↩ -
Microsoft Learn, Process.PrivateMemorySize64 Property. On
PrivateMemorySize64returning the memory private to the process that cannot be shared with other processes, corresponding to the Private Bytes performance counter. ↩ -
Microsoft Learn, Process.VirtualMemorySize64 Property. On
VirtualMemorySize64returning the amount of virtual memory allocated for the process, corresponding to the Virtual Bytes performance counter. ↩ -
Microsoft Sysinternals, VMMap. On breaking down a process’s committed virtual memory by type, and displaying the physical memory (Working Set) allocated to each, along with a detailed memory map. ↩ ↩2
Related Articles
Recent articles sharing the same tags. Deepen your understanding with closely related topics.
A Practical Guide to Process Monitor (ProcMon) — Pinpointing "Settings Not Applied" and "ACCESS DENIED" in 10 Minutes
"I fixed the config file, but nothing changed." "It worked yesterday, but won't start today." Before touching the source code, Process Mo...
Using WMI/CIM from C# and PowerShell — A Practical Guide to Hardware Info Retrieval, Process Monitoring, and Remote Queries
WMI/CIM is the standard way to get a PC's serial number, monitor free disk space, and detect process launches. This article covers how to...
Investigating Event Logs in Practice with Get-WinEvent — Filtering Speed Decides How Long the Investigation Takes
How to make Windows event log investigation efficient with PowerShell. Covers why filtering with Where-Object is slow, when to use Filter...
Process Explorer / Handle / VMMap in Practice — Chasing Hangs, Leaks, and "File in Use" from the State Right Now
Part two of our practical Sysinternals series, covering "it gets slower and slower," "the file can't be deleted," and "it hung." We look ...
Windows Time Synchronization (w32time) and Business Systems — Solving "The Log Timestamps Don't Match" from the Mechanism Up
Why do timestamps drift between a device and a PC? This article explains it from the mechanics of the Windows Time service (w32time): the...
Related Topics
These topic pages place the article in a broader service and decision context.
Windows Technical Topics
Topic hub for KomuraSoft LLC's Windows development, investigation, and legacy-asset articles.
Where This Topic Connects
This article connects naturally to the following service pages.
Windows App Development
We support Windows desktop applications that involve resident processing, device integration, operational logging, and maintainable structure.
Frequently Asked Questions
Common questions about the topic of this article.
- Does Task Manager's "Memory" column show the total memory an app has allocated?
- No. Task Manager has several memory columns — the Working Set family, the Private Working Set family, Commit Size, and others — and the meaning depends on which screen and column you are looking at. Working Set is the pages currently resident in RAM; Private Bytes or Commit Size is that process's own committed amount. Do not read a single "Memory" column as the total capacity an app has allocated, or as the size of a leak.
- What is the difference between Working Set and Private Bytes?
- Working Set is the amount of pages visible to that process that are currently resident in physical RAM, including shareable pages such as DLL code and memory-mapped files. Private Bytes is the amount of committed memory used exclusively by that process, regardless of whether it is currently resident in RAM. The two therefore never equal each other, and neither is consistently larger than the other.
- Does Task Manager's "Committed 18/32GB" mean that 18GB has been written to the page file?
- No. The left-hand figure is the total commit that the whole system is currently promising to back; the right-hand figure is the commit ceiling the system can support. The ceiling is determined roughly by RAM plus the page file, but not all of the left-hand total actually sits in the page file. Most committed pages are in RAM, and some committed pages have never had a physical page assigned to them at all. Meanwhile, pages that can be reloaded from their original file — such as EXEs, DLLs, and memory-mapped files — do not necessarily raise private Commit by the same amount they raise Working Set.
- Can you get OutOfMemory even when free RAM is available?
- Yes. Allocation can fail for reasons other than physical RAM, including a 32-bit process running out of virtual address space, a shortage of contiguous free address range, the system commit ceiling, or limits specific to a Job Object or runtime. In particular, a 32-bit process on 64-bit Windows is normally capped at 2GB of user-mode virtual address space unless it is Large Address Aware.
- Does disabling the page file make Windows faster?
- You cannot assume it will, as a general rule. Disabling the page file lowers the system's commit ceiling, makes it harder to evict unused modified pages from RAM, and affects how crash dumps can be configured. Page file size should be decided by measuring peak commit charge and the crash dump you need — it is not a setting to disable without justification.
- Does a high Page Faults/sec mean the system is short on memory?
- You cannot tell from that alone. Page faults include soft faults, which can be resolved from Standby pages in RAM or pages shared with another process, and hard faults, which read from disk. Rather than looking at Page Faults/sec in isolation, check Pages Input/sec, Page Reads/sec, Available MBytes, disk latency, and processing time together on the same timeline.