Managed Memory Model in the .NET Framework

  • 2/18/2009
This chapter from Solid Code is a practical discussion of the Garbage Collector and the memory mode of the .NET Framework.
  • The first rule of management is delegation. Don’t try and do everything yourself, because you can’t.

  • Anthea Turner

In managed code, garbage collection is delegated to the Common Language Runtime (CLR). The Garbage Collector (GC) is a component of the CLR and responsible for managing managed memory. This chapter is a practical discussion of the Garbage Collector and the memory mode of the .NET Framework. In managed code, memory is allocated on demand for dynamic objects with the new operator. However, the Garbage Collector is responsible for freeing the memory for that object when necessary. There is no delete operator as in the C++ language.

In C++, the developer was responsible for managing dynamic memory. Dynamic memory is allocated at run time. The new and delete operators exist for this reason. The new operator allocates memory, while the delete operator frees memory. There are also advanced techniques for allocating dynamic memory in native code. HeapCreate, HeapAlloc, HeapFree, HeapDestroy, and related functions are used to create and obtain memory from a native heap. To access virtual memory directly, there is VirtualAllocEx and VirtualFreeEx. For memory mapped files, the application programming interfaces (APIs) CreateFile, CreateFileMapping, and MapViewOfFileEx are available. Pointers are the common thread through the various options to allocate memory at run time. Historically, mismanagement of pointers has been the reason for untold problems, such as memory leaks and memory corruption. The C++ dynamic memory model overly involved the developer. The primary goal of the developer is to create a solution to a problem, not to manage pointers. Managed code allows developers to focus on solving problems rather than on the intricacies of memory management.

Memory management for managed code is the responsibility of the developer and the Garbage Collector. The developer is responsible for allocating objects, while the Garbage Collector is responsible for freeing objects. When objects are created at run time, they reside on the managed heap. You refer to an object with a reference, which is an abstraction of a pointer. The reference abstracts the developer writing managed code from managing pointers, which prevents pointer-related problems. In this way, the developer delegates to the CLR and the Garbage Collector to manage the managed heap and pointers. This delegation is an important shift in responsibility in the managed environment from the native environment.

Two basic assumptions dominate the memory management model of .NET. Large objects are long-lived objects. Similarly sized objects are more likely to communicate with each other. For these reasons, the Garbage Collector uses a concept called generations to group objects by size and age. As a practice, architect your program to match these assumptions. This will adversely affect the performance of garbage collection and, consequently, your application.

Memory utilization in .NET revolves around the managed heap. When you allocate a new object, it is placed on the managed heap. When unreachable, the Garbage Collector will free that object. That will reclaim the memory for that object on the managed heap.

Managed Heap

The managed heap is partitioned into generations and the Large Object Heap. Generations 0, 1, and 2 are used to group objects by size and age. The ephemeral generations exclude the oldest generation. At the moment, the ephemeral generations include Generations 0 and 1. The reason for the distinction is that the ephemeral generations and the oldest generation sometimes can behave differently. Generation 0 is the smallest generation, Generation 1 is medium sized, while Generation 2 is the largest. For this reason, it is more likely that larger objects will appear in Generations 1 and 2. Generation 0 is simply not large enough to hold many larger objects.

The managed heap is partitioned into large objects and everything else. Large objects are greater than 85,000 bytes (85 KB) and reside on the Large Object Heap. This is not documented and is subject to change. Everything else resides on Generation 0, 1, or 2.

The Garbage Collector is responsible for freeing memory during a garbage collection. There are three events that initiate garbage collection. First is an allocation that, if successful, would exceed the memory threshold of Generation 0. Objects are always allocated to Generation 0. You cannot directly place an object on Generation 1 or 2. Because objects always start their life at Generation 0, it holds the youngest objects. Second is allocating a large object when there is insufficient memory available on the large object heap. The third event is calling the GC.Collect method. This will force garbage collection on demand.

The Garbage Collector collects the generations in order: Generation 0, 1, and then 2. Whenever a generation is collected, the younger generations of that generation are also collected. If Generation 1 is collected, then Generation 0 is also collected. Collecting Generation 2, which is considered a full collection, will also collect the ephemeral generations. This approach means that younger generations are collected more frequently than the older generations. This is designed for efficiency since the older objects tend to reside on the larger generations. Collecting, reclaiming, and compacting the memory for larger generations is more costly than for smaller generations. Another advantage to this model is the ability to collect a portion of the heap. Partitioning the managed heap generation supports this behavior. You can collect one or more generations and avoid a full collection of the managed heap, which is, naturally, expensive.