Memory usage from !dumpheap -stat does not equal total memory used
Categories:
Demystifying .NET Memory Usage: Why !dumpheap -stat Doesn't Equal Total Memory

Explore the discrepancies between !dumpheap -stat
output and total process memory in .NET applications, and learn how to accurately diagnose memory issues using WinDbg.
When debugging .NET memory issues, developers often turn to WinDbg and the SOS extension. A common first step is to use the !dumpheap -stat
command to get a summary of managed heap objects. However, it's a frequent source of confusion when the total size reported by !dumpheap -stat
is significantly less than the total memory reported by Task Manager or other system monitoring tools for the same process. This article explains why this discrepancy occurs and how to get a more complete picture of your application's memory footprint.
Understanding Managed vs. Unmanaged Memory
The core reason for the discrepancy lies in the distinction between managed and unmanaged memory. The !dumpheap -stat
command, provided by the SOS extension, specifically reports on objects allocated on the .NET managed heap. This includes all objects that the Garbage Collector (GC) tracks and manages, such as strings, arrays, custom classes, and other reference types.
However, a .NET process consumes much more memory than just its managed heap. It also uses a significant amount of unmanaged memory. This unmanaged memory is not directly controlled by the .NET runtime's Garbage Collector and includes:
- Native allocations: Memory allocated by the operating system or native libraries (e.g., Win32 APIs, COM objects, P/Invoke calls).
- JIT-compiled code: The memory used to store the machine code generated by the Just-In-Time (JIT) compiler.
- Thread stacks: Each thread in the process requires its own stack memory.
- Loader heaps: Used by the CLR for internal data structures, metadata, and other runtime components.
- Memory-mapped files: Files or resources mapped directly into the process's address space.
- Graphics/UI resources: Memory used by GDI+, DirectX, or other UI frameworks.
- Off-heap allocations: Large objects that might be allocated outside the regular managed heap by certain frameworks or libraries (e.g., some
System.IO.MemoryStream
implementations,System.Buffers.ArrayPool
).
flowchart TD A[Total Process Memory] --> B{Managed Heap} A --> C{Unmanaged Memory} B --> D[Objects tracked by GC] C --> E[Native Allocations] C --> F[JIT Code] C --> G[Thread Stacks] C --> H[Loader Heaps] C --> I[Memory-Mapped Files] C --> J[Graphics/UI Resources] C --> K[Off-Heap Buffers] D[Objects tracked by GC] --> L["!dumpheap -stat" reports this] C[Unmanaged Memory] --> M[Other tools report this + Managed Heap]
Breakdown of Total Process Memory
Diagnosing Total Memory Usage with WinDbg
To get a more comprehensive view of memory usage in WinDbg, you need to look beyond !dumpheap -stat
. While there isn't a single command that perfectly sums up all unmanaged memory, you can use a combination of commands to identify significant contributors.
!address -summary
: This command provides a high-level overview of the process's virtual memory usage, breaking it down by region type (e.g., Image, Stack, Heap, Private). It's an excellent starting point to see where large chunks of memory are being consumed.!eeheap -loader
and!eeheap -gc
: These commands provide details about the CLR's internal heaps.!eeheap -loader
shows memory used by the loader heaps (for metadata, JIT code, etc.), and!eeheap -gc
shows the managed heaps (which!dumpheap -stat
also reports on, but!eeheap -gc
gives more detail on heap segments).!for_each_heap !heap -s
(for native heaps): If your application uses native heaps extensively (e.g., through P/Invoke or C++/CLI), this command can help summarize their usage. Note that this is a more advanced technique and might require deeper understanding of native debugging.!sym noisy
and!dlls
: To identify loaded modules and their sizes, which contribute to the 'Image' section in!address -summary
.
By combining the output of these commands, you can start to piece together the full memory picture and identify if a memory leak is occurring in managed or unmanaged code.
0:000> !address -summary
--- Usage Summary ---------------- Parameters --------
Tot Size ( KB) Pct Usage Dom Count
7ff`fffff (2097151) 100.00% <unallocated> 1
100`00000 (1048576) 50.00% Image <unknown> 1
1`000000 ( 16384) 0.78% Stack <unknown> 1
1`000000 ( 16384) 0.78% Heap <unknown> 1
... (truncated for brevity)
0:000> !eeheap -loader
Loader Heap: Start End Size
Domain 000001F292B00000: 00007ff8`d0000000 00007ff8`d0001000 0x1000 ( 4 KB)
Domain 000001F292B00000: 00007ff8`d0001000 00007ff8`d0002000 0x1000 ( 4 KB)
... (truncated)
Total size: 0x123456 (1193046 bytes)
0:000> !eeheap -gc
Number of GC Heaps: 1
------------------------------
Heap 0 (000001F292B00000)
segments in use: 3
segment 0: 000001F292B00000 000001F292B00000 000001F292B00000 (0x0 bytes)
segment 1: 000001F292B00000 000001F292B00000 000001F292B00000 (0x0 bytes)
segment 2: 000001F292B00000 000001F292B00000 000001F292B00000 (0x0 bytes)
Total size: 0xABCDEF (11259375 bytes)
------------------------------
GC Heap Size: 0xABCDEF (11259375 bytes)
Example WinDbg output for memory analysis
!dumpheap -stat
with total process memory, remember that !dumpheap -stat
only accounts for the managed heap. Always consider unmanaged allocations, JIT code, and other runtime overheads.Common Scenarios for Discrepancies
Several common scenarios can lead to a large gap between managed heap size and total process memory:
- Heavy P/Invoke Usage: Applications that frequently call native APIs or interact with unmanaged code (e.g., graphics libraries, custom C++ DLLs) will have significant unmanaged memory footprints.
- Large Native Buffers: Using
Marshal.AllocHGlobal
or similar methods to allocate large unmanaged buffers for interop scenarios. - Graphics-Intensive Applications: Games or UI-heavy applications often allocate large amounts of memory for textures, frame buffers, and other graphical resources outside the managed heap.
- High Thread Count: Each thread consumes stack space, and a large number of threads can add up to a substantial amount of unmanaged memory.
- JIT Code Bloat: Complex applications with many methods can accumulate a large amount of JIT-compiled code in memory.
- Memory-Mapped Files: Applications working with large files by mapping them into memory will show high memory usage not reflected in
!dumpheap -stat
.
1. Attach WinDbg to the process
Start WinDbg and attach it to your running .NET application process. Load the SOS extension using .loadby sos clr
(for .NET Core/5+) or .loadby sos mscorwks
(for .NET Framework).
2. Get managed heap statistics
Run !dumpheap -stat
to get a baseline of your managed memory usage. Note the total size reported.
3. Analyze virtual memory summary
Execute !address -summary
to understand the overall virtual memory layout and identify large regions like 'Image', 'Stack', and 'Heap'.
4. Inspect CLR internal heaps
Use !eeheap -loader
and !eeheap -gc
to get more detailed information about the CLR's internal memory allocations, including JIT code and GC heap segments.
5. Correlate and investigate
Compare the numbers. If !address -summary
shows a large 'Heap' or 'Private' region not accounted for by !dumpheap -stat
or !eeheap
, you might have significant native allocations. Further investigation with native debugging tools or specific WinDbg commands (e.g., !heap -s
for native heaps) may be necessary.