“How much memory is your process using?” — I bet you were asked that question, or asked it yourself, more times than you can remember. But what do you really mean by memory?
I never thought it would be hard to find a definitive resource for what the various memory usage counters mean for a Windows process. But try it: Google “Windows Task Manager memory columns,” and you’ll see confusing, conflicting, inconsistent, unclear explanations of what the different metrics represent. If we can’t even agree on what “working set” or “commit size” means, how can we ever monitor our Windows applications successfully?
First, we will need a sample application that will allocate various kinds of memory for our experiments. I’ve written one for this blog post: it is simply called Memory. You can find it on GitHub. Currently, it supports multiple kinds of allocations: reserve, commit, shareable memory, and more.
To monitor application memory usage, we will use Sysinternals VMMap, a long-time favorite on my blog. It offers unparalleled insight into what your application is doing in terms of memory. Simply choose a process when launching VMMap, and view memory utilization categorized by type (private, shared, reserved, committed) and purpose (image, heap, stack, mapped file). You can also run it from the command line, for example:
VMMap.exe -p MyApp output.csv
Armed with these tools, let’s get to business and try to characterize the various kinds of memory usage in Windows processes. We must begin with the virtual memory size of the process — the amount of address space that is in use.
Windows applications do not access physical memory directly. Any address in your application is a virtual address that is translated by the CPU to a physical address when accessed. Although it is often the case that there is more virtual memory available than RAM to back it up, virtual memory is still limited. On 32-bit Windows with a default configuration, each process can allocate up to 2GB of virtual memory. On 64-bit Windows, each 64-bit process can allocate up to 128TB of virtual memory (this limit used to be 8TB until Windows 8.1).
Each page of virtual memory can be in one of three states: free, reserved, and committed:
Free pages is available for subsequent allocations (and excluding unusable pages, discussed later).
Reserved pages are not available for subsequent allocations, but they are not backed by physical memory. In other words, you may not access reserved pages, and you may not assume that at some point the system will have sufficient physical memory to back them up. For example, try running Memory.exe reserve 10000000 to allocate approximately 10TB of reserved memory. This should work just fine (on a 64-bit system, of course), although you probably don’t have enough physical memory to back up 10TB of virtual addresses.
Committed pages may be accessed by your application. The system guarantees that when you access a committed page, there will be physical memory to back it up. The physical memory is allocated on-demand, when you first access the page. Even though the system doesn’t allocate physical memory immediately, this guarantee implies that there is a system-wide limit on how much memory can be committed by all processes. This limit is called the commit limit. If an allocation would exceed the commit limit, the system does not satisfy it. Go ahead and try it: Memory.exe commit 10000000.
To further complicate things, committed memory can be shared with other processes. If two processes share 100MB of physical memory, the 100MB virtual region is committed in both processes, but it only counts once towards the commit limit.
It makes sense to examine the following aspects of a process’ virtual memory usage:
Committed bytes. This information is available in VMMap under Total > Committed and the Process > Page File Bytes performance counter.
Reserved bytes. This information is available in VMMap as the delta between Total > Size and Total > Committed. It can be calculated as the difference between non-free bytes and committed bytes.
Non-free bytes. This information is available in VMMap under Total > Size, or as the Process > Virtual Bytes performance counter.
Free bytes. This information is available in VMMap under Free > Size. It can also be deduced from the size of the virtual address space (2GB, 3GB, 4GB, 8TB, or 128TB — depending on the system configuration), and the non-free bytes value.
This tells almost the whole story. Here’s a statement that at this point might sound fairly accurate:
The non-free bytes value is exactly the amount of virtual memory that is available for subsequent allocations.
Unfortunately, it is not entirely accurate. The Windows memory manager guarantees (for historical reasons) that new allocations are aligned on a 64KB boundary. Therefore, if your allocations are not all divisible by 64KB, some memory regions might be lost for future allocations. VMMap calls them Unusable, and it is the only tool that can reliably display them. To experiment, run Memory.exe unusable 100. VMMap will report around 100MB of unusable virtual memory, which is theoretically free and invisible to any other tool. However, that memory cannot be used to satisfy future allocations, so it is as good as dead.
As I noted earlier, physical memory can be shared across multiple processes: more than one process may have a virtual page mapped to a certain physical page. Some of these shared pages are not under your direct control, e.g. DLL code is shared across processes; some other shared pages can be allocated directly by your code. The reason it’s important to understand shared memory usage is that a page of shared memory might be mistakenly attributed to all processes sharing that page. Although it definitely occupies a range of virtual addresses in each process, it’s not duplicated in physical memory.
There’s also a matter of terminology to clarify here. All shared pages are committed, but not all committed pages can be shared. A shareable page must be allocated in advance as part of a section object, which is the kernel abstraction for memory-mapped files and for sharing memory pages across processes. So, to be precise, we can speak of two kinds of shareable memory:
Shareable memory that is shared: memory pages that are currently mapped into the virtual address space of at least two processes.
Shareable memory that is not currently shared: memory pages that may be shared in the future, but are currently mapped into the virtual address space of fewer than two processes.
NOTE: The terms “private” and “shared” (or “shareable”) memory refer only to committed memory. Reserved pages cannot be shared, so it makes no sense to ask whether they are private or shareable.
It makes sense to look at the following per-process data points, to understand which part of its virtual memory is shared (or shareable) with other processes:
Private bytes (memory that is not shared or shareable with other processes). This information is available in VMMap under Total > Private. It is also available as a performance counter Process > Private Bytes. Note that some of this committed memory may be backed by the page file, and not currently resident in physical memory.
Shareable bytes. This information is available in VMMap under Shareable > Committed. You can’t tell which of these bytes are actually shared with other processes, unless you settle for the following two data points:
Shareable bytes currently resident. This information is available in VMMap under Total > Shareable WS, but only includes pages that are resident in physical memory. It doesn’t include potentially-shareable pages that happen to be paged out to disk, or that weren’t accessed yet after being committed.
Shared bytes currently resident. This information is available in VMMap under Total > Shared WS, but again only includes pages that are resident in physical memory.
Also note that VMMap’s Shareable category doesn’t include certain kinds of shareable memory, such as images (DLLs). These are represented separately by the Image category.
Try it out: run Memory.exe shareable_touch 100. You’ll see private bytes unchanged, and shareable bytes go up — even though the allocated memory isn’t currently shared with any other process. Shared bytes, on the other hand, should remain the same. You can also try Memory.exe shareable 100 — you’ll see the Shareable/Shared WS values unchanged because physical memory is not allocated unless the committed memory is also accessed.
So far, we only discussed the state of virtual memory pages. Indeed, free, unusable, and reserved pages have no effect on the amount of physical memory used by the system (other than the data structures that must track reserved memory regions). But committed memory may have the effect of consuming physical memory, too. Windows tracks physical memory on a system-wide basis, but there is also information maintained on a per-process level that concerns that process’ individual physical memory usage through its set of committed virtual memory pages, also known as the working set.
Windows manages physical memory in a set of lists: active, standby, modified, free, and zero — to name a few. These lists are global to all processes on the system. They can be very important from a monitoring standpoint, but I’ll leave them for another time. If you’re really curious, there’s a great Sysinternals tool called RAMMap that you can explore.
We need to add to our monitoring toolbox the following data points related to process physical memory:
Private physical bytes. This refers to the physical pages that are mapped to private committed pages in our process, and is often called the process’ private working set. This information is available in VMMap under Total > Private WS. It is also available in Task Manager as Memory (private working set).
Shareable or shared physical bytes. Similarly, these are the physical pages that are mapped to shareable committed pages in our process. We discussed these metrics before when talking about shareable/shared memory (in VMMap, these are under Total > Shared/Shareable WS).
Total physical bytes. Simply the sum of the previous two metrics. You might be tempted to say that this is the amount of physical memory consumed by our process, which would be accurate if it wasn’t for sharing. This information is available in VMMap under Total > Total WS, as the Process > Working Set performance counter, and in Task Manager as Working set (memory).
Committed bytes not mapped yet to any backing storage (RAM or page file). Like I said before, Windows doesn’t allocate any physical memory when you commit a page of virtual memory. Only when the virtual page is first accessed, Windows handles the hardware exception by lazily allocating a page of physical memory. So, you could have committed pages in your process that aren’t currently backed by neither RAM nor page file — simply because they were never accessed until now. Unfortunately, there is no easy way that I know of to get this information.
You can experiment with the on-demand physical memory allocation by running Memory.exe commit 1000. Even though the system-wide commit size was charged 1000MB, you won’t see any change in physical memory usage (e.g. in Task Manager). But now try Memory.exe commit_touch 1000, which commits memory and makes sure to touch every single page. This time, both the commit size and physical memory usage should go up by 1000MB.
Committed bytes not currently resident. These are pages of committed memory that was paged out to disk. If you’re willing to ignore committed pages that weren’t accessed yet, then this metric can be calculated as the difference between VMMap’s Total > Committed and Total > Total WS values (or as the difference between the Process > Page File Bytes and Process > Working Set Bytes performance counters — recall that Process > Page File Bytes is really the commit size of the process).
Finally, your process can indirectly affect the system’s memory usage, too. Kernel data structures like files, sockets, and mutexes are created and destroyed when your process requests it. Page tables that map virtual to physical addresses are allocated and populated when you commit and access memory pages.
Although it is rarely the case that your process would make a significant dent in the kernel’s memory usage, it’s important to monitor the following metrics:
Pool bytes. This refers to kernel memory directly attributable to your process, such as data structures for files or synchronization objects. Pool memory is further subdivided to paged pool and non-paged pool. The system-wide pool utilization values are available in Task Manager (under the Memory tab), or as the Memory > Pool Paged Bytes and Memory > Pool Nonpaged Bytes performance counters.
For some kernel objects, the pool allocation is also charged to the owning process. This is the case with I/O completion packets queued to an I/O completion port, which is what you can experiment with by running Memory.exe nppool 10000 and inspecting the value of the Process > Pool Nonpaged Bytes performance counter. (To quickly inspect performance counters, run typeperf from a command prompt window. For example: typeperf “Process(Memory)\Pool Nonpaged Bytes” will show you the counter’s value every second.)
Page table bytes. Mapping virtual addresses to physical addresses requires book-keeping, provided by data structures called page tables. These data structures are allocated in kernel memory. At a high level, mapping a small page of virtual memory (4KB on both x86 and x86_64) requires 4 or 8 bytes of page table space, plus some additional small overhead. Because Windows is lazy, it doesn’t construct page tables in advance when you reserve or even commit memory. Only when you actively access a page, Windows will fill the page table entry. Page table usage is available in VMMap as Page Table > Size. It would typically be a fairly small value, even if you allocate a lot of memory.
Experiment by running Memory.exe commit_touch 2000 (committing and touching almost 2GB of memory). On my Windows 10 x64 system, the resulting increase in page table bytes was approximately 4MB.
NOTE: Because any virtual memory allocation has the potential of requiring page table space eventually, Windows used to charge reserved memory to the system commit limit, because it anticipated these reserved pages to eventually become committed and require actual page table space. In Windows 8.1 x64 and Windows 10 x64, a security mechanism called CFG (Control Flow Guard) requires a 2TB chunk of reserved memory for each process. Charging commit for that many pages would be impractical. Therefore, on newer versions of Windows, reserving memory does not charge commit. You can verify this by running Memory.exe reserve 1000000 (to reserve almost 1TB of memory) and note that the system-wide commit limit (typeperf “Memory\Committed Bytes”) doesn’t go up considerably.
Hopefully, this post explained the key memory monitoring metrics for Windows processes. There’s a lot more to say about the internals of Windows memory management, and I’m happy to refer you to Windows Internals, 6th Edition for more details. You might also find the Testlimit tool useful to check just how far the memory manager is willing to stretch for you.