To evaluate whether your application can scale with larger data sets and more concurrent users, you have to understand how it uses the memory available to it. Modern .NET applications (and especially mixed managed and native applications) have thousands of memory chunks across their address space—keeping track of these chunks manually in your head is an impossible endeavor.
First, a quick reminder about how virtual memory, managed heap allocations, and native heap allocations relate to each other (for more details, consider reading the Memory Manager chapter in Windows Internals):
- A Windows process has a virtual address space, which is a range of addresses your application can use. On 32-bit Windows, the address space available to your code is usually 2GB (roughly, the addresses 0x00000000 – 0x7FFFFFFF); on 64-bit systems, the address space available to your 64-bit process is 8TB.
- There is no requirement that enough physical memory (RAM) be available to support the virtual memory requirements of all applications. For example, you can install 2GB of physical memory on your PC and still run dozens of processes, each with a 2GB address space. Windows maps virtual addresses to physical addresses as demands dictate, and transfers data from physical memory to the disk (page file) to free physical memory space.
- Virtual memory can be allocated in two steps, using the VirtualAlloc Win32 API.
- You can tell Windows to reserve a chunk of virtual memory addresses—this reservation is very cheap, and doesn’t incur any physical memory cost. Windows guarantees that no other component in your process will use the same address range.
- After reserving a chunk of memory, you can commit it—this is when you can start reading and writing to the allocated address range.
- The VirtualAlloc API is not sufficiently granular for high-level language use (64KB start address granularity, 4KB allocation granularity), and incurs a system call for each allocation request.
- Most applications don’t allocate virtual memory directly.
- Managed applications use the .NET allocator (new operator), which reserves from Windows large chunks of memory called GC segments and manages allocations within these segments, committing parts of them as necessary.
- Native applications use the Windows Heap Manager (HeapAlloc) or the CRT allocator (malloc, C++ new operator), which operates similarly to the .NET allocator.
What kinds of things can you expect to see in your process’ memory space?
- Loaded DLLs – code your application uses has to be mapped to your process’ virtual memory. In large applications, a significant portion of the address space can be taken by DLLs.
- Native heaps – allocations performed by HeapAlloc, malloc, or the C++ new operator.
- Managed heaps – allocations performed by the .NET allocator.
- JIT heaps – areas in which just-in-time compiled code is stored (recall that .NET assemblies may contain platform-independent IL, which is compiled at runtime to executable machine code).
- Mapped files – files that your applications maps to virtual memory for easier access without using stream-based APIs.
- Thread stacks – each Windows thread starts with a 1MB default stack reservation, of which 4KB (a single page) is initially committed.
In the second part we’ll start mapping the memory usage of .NET applications and visualizing the various allocation types.