Every program needs memory. Unfortunately, memory is finite.
Software must cope with memory usage, and there are two ways to manage it: manually and automatically. Manual memory management is prone to errors, especially with exceptions and asynchronous code. This is why modern managed environments (.NET, Erlang, and many more) implement automatic memory management with garbage collection. Figure 1
No object or application root in Figure 2 has a reference to Object D or Object H. In the garbage collection process, the garbage collector discards both of these objects then compacts the heap.
There is a potential issue with this scenario. Assuming Object E occupies a large amount of memory, e.g. 300 MB, the performance will be suboptimal when the garbage collector moves Object E to Object D’s memory location.
In .NET objects are divided into two groups:
- Large Objects – larger than 85KB or multidimensional arrays
- Small Objects – everything else
The Common Language Runtime (CLR) allocates large objects on the Large Object Heap (LOH). The primary difference with the previous algorithm is that the garbage collector never compacts this heap. This increases performance, but there is a catch. There may not be enough room for a new object even with enough available memory. This causes the CLR to throw an inappropriately named OutOfMemoryException.
The garbage collector uses three segments, called generations, for small objects:
- Generation 0
- Generation 1
- Generation 2
The CLR allocates memory for new objects in Generation 0. When this heap is full, the garbage collector discards objects no longer in use and promotes surviving objects (referred to as live objects) to Generation 1. The Generation 0 heap is then available to hold more newly created objects.
When insufficient, the garbage collector repeats this process for Generation 1, and if still insufficient, continues with Generation 2. The garbage collector removes non-referenced objects and promotes surviving objects for Generation 1. This behavior differs in Generation 2 as it contains the longest surviving objects. After garbage collection completes, the garbage collector compacts the heaps.
In Figure 4, an application root references Object F in Generation 0 and the garbage collector promotes it to Generation 1. Before the next garbage collection, the application root dereferences Object F, and the garbage collector frees it but promotes objects reachable from an application root to Generation 2.
When called, the garbage collector builds a graph of reachable objects. JustTrace displays a graphical representation of references to an object from root objects in the Root Paths view.
Figure 5 shows the Root Paths view for a ResourceDictionary instance.
- Click the Force GC button in JustTrace and wait for garbage collection to finish before taking a snapshot. Some objects may be marked for Finalization if they have Finalize() method. To free the memory of these objects, the garbage collector needs to wait for two garbage collections to take place. Taking a snapshot calls garbage collection, so both take place by following this tip. The garbage collector will also collect objects referenced by the object marked for finalization.
- Be careful when using pinned objects (fixed in C#). A pinned object holds a pointer to its location on the heap. The garbage collector begins to collect objects after pinned objects, and objects pinned near the end of the heap may cause an OutOfMemoryException although objects before the pinned object are available for collection. Always use pinned objects for a short duration then close or dispose them when finished.