.NET Garbage Collection Concepts
.NET allocates memory faster than the standard C runtime
malloc and Windows
HeapAlloc functions. In fact, allocating memory is about as fast allocating memory from the thread’s stack. There is no traversal of a linked list to find a block of memory large enough to satisfy a memory request. Instead, an internal pointer is simply advanced to point to the next place in the private managed heap where a future memory request will be satisfied.
This feature actually provides a second major benefit: locality of reference, meaning that objects allocated around the same time will be physically proximate. So in addition to faster allocation, there is improved runtime performance because of drastically better cpu cache performance (avoiding slower RAM access) and far fewer page faults.
There is a further optimization that large objects (>20K) are allocated from a special heap. See Richter’s articles (below) for more details.
Of course, unless a program’s memory requirements are extremely modest, sooner or later a request for memory will fail. That is when the garbage collector will be invoked. Let me repeat this another way since there is a misconception that having a garbage collector means a background thread is waking up periodically to do some housekeeping: there is no garbage collector “heartbeat” – nothing running in the background on some schedule. The garbage collector runs when it becomes necessary to reclaim memory to satisfy new memory request.
Where some people might get confused is that a separate thread will peform special “finalization” of objects that implement a
finalize method, but only after the collector has marked them for collection and added them to a special internal queue. A special thread is awakened when objects are placed on this queue so that their
finalize method can be executed. Therefore, this thread only runs after a garbage collection cycle places objects with finalizers in this special queue, and garbage collection only happened because a memory request could not be satisfied in the first place.
As already discussed, the garbage collector doesn’t run until a request for memory (
new) fails. At that point, the collector begins to look for candidates to reclaim. Conceptually this is very simple: if objects on the heap have nothing pointing to them (because references to them went out of scope or were set to null), then they are garbage and can be reclaimed.
The .NET implemenation of checking for garbage is heavily optimized and happens very quickly. The garbage collector does not generally need to scan every object on the heap. It maintains three separate graphs of objects representing generations. Initially, there is only generation 0. The first time the collector makes a collection pass, objects that survive are promoted to generation 1. Survivors of subsequent passes are promoted to generation 2. The principle is simple. Long lived objects don’t need to be checked as frequently since they are, well, long-lived. If they are garbage collection survivors, then subsequent passes won’t waste time looking at them unless memory is getting so low that a pass through one generation didn’t reclaim enough.
Microsoft invested a great deal of effort in ensuring that the .NET garbage collector performed as optimally as possible. As Richter points out, the performance six years ago was already impressive:
Microsoft’s performance tests show that managed heap allocations are faster than standard allocations performed by the Win32 HeapAlloc function. These tests also show that it takes less than 1 millisecond on a 200Mhz Pentium to perform a full GC of generation 0. It is Microsoft’s goal to make GCs take no more time than an ordinary page fault.
Many people don’t realize that the net result is that the penalty for memory management in unmanaged code can actually be greater than for managed, garbage-collected code. One argument for manual memory management is that the penalty for manually freeing memory is more deterministic than garbage collection. Ironically, manually allocating memory is actually less deterministic (for standard memory allocators, such as provided by the standard C runtime library).