Improving .NET Application Performance Part 4: Garbage Collection

This post is a part of a series of posts on .NET Application performance. The first three articles are also available on our blog: Part 1Part2 ; Part 3

In this article of our series on improving .NET application performance and in this article we’ll discuss garbage collection guidelines. The .NET Framework uses automatic garbage collection to manage memory for all applications. When you use the New operator to create an object, the object’s memory is obtained from the managed heap. When the garbage collector decides that sufficient garbage has accumulated that it is efficient to do so, it performs a collection to free some memory. This process is fully automatic, but there are a number of things you need to be aware of.

Let’s take a look at the lifecycle of a managed object:

  1. Memory for an object is allocated from the managed heap when you call new. The object’s constructor is called after the memory is allocated.
  2. The object is used for a period of time.
  3. The object dies due to all its references either being explicitly set to null or else going out of scope.
  4. The object’s memory is freed (collected) some time later. After the memory is freed, it is generally available for other objects to use again.


The managed heap can be thought of as a block of contiguous memory. When you create a new object, the object’s memory is allocated at the next available location on the managed heap. Because the garbage collector does not need to search for space, allocations are extremely fast if there is enough memory available. If there is not enough memory for the new object, the garbage collector attempts to reclaim space for the new object.


To reclaim space, the garbage collector collects objects that are no longer reachable. An object is no longer reachable when there are no references to it, all references are set to null, or all references to it are from other objects that can be collected as part of the current collection cycle.

When a collection occurs, the reachable objects are traced and marked as the trace proceeds. The garbage collector reclaims space by moving reachable objects into the contiguous space and reclaiming the memory used by the unreachable objects. Any object that survives the collection is promoted to the next generation.


The garbage collector uses three generations to group objects by their lifetime and volatility:

  • Generation 0 – This consists of newly created objects. Gen 0 is collected frequently to ensure that short-lived objects are quickly cleaned up. Those objects that survive a Gen 0 collection are promoted to Generation 1.
  • Generation 1 – This is collected less frequently than Gen 0 and contains longer-lived objects that were promoted from Gen 0.
  • Generation 2 – This contains objects promoted from Gen 1 (which means it contains the longest-lived objects) and is collected even less frequently. The general strategy of the garbage collector is to collect and move longer-lived objects less frequently.

Garbage Collection Guidelines

In this section we’ll give you some guidelines to improve the garbage collection performance.

Analyze Your Application’s Allocation Profile – Object size, number of objects, and object lifetime are all factors that impact your application’s allocation profile. While allocations are quick, the efficiency of garbage collection depends (among other things) on the generation being collected. Collecting small objects from Gen 0 is the most efficient form of garbage collection because Gen 0 is the smallest and typically fits in the CPU cache. In contrast, frequent collection of objects from Gen 2 is expensive. To identify when allocations occur, and which generations they occur in, observe your application’s allocation patterns by using an allocation profiler such as the CLR Profiler.

Avoid Calling GC.Collect  The default GC.Collect method causes a full collection of all generations. Full collections are expensive because literally every live object in the system must be visited to ensure complete collection. Exhaustively visiting all live objects usually takes a significant amount of time. The garbage collector is tuned so that it does full collections only when it is likely to be worth the expense of doing so. As a result, do not call GC.Collect directly — let the garbage collector determine when it needs to run.

The garbage collector is designed to be self-tuning and it adjusts its operation to meet the needs of your application based on memory pressure. Programmatically forcing collection can hinder tuning and operation of the garbage collector.

Consider Using Weak References with Cached Data – Consider using weak references when you work with cached data, so that cached objects can be resurrected easily if needed or released by garbage collection. Use weak references mostly for objects that are not small in size because the weak referencing itself involves some overhead. They are suitable for medium to large-sized objects stored in a collection.

If on a subsequent cache lookup, you cannot find the object, re-create it from the information stored in an authoritative persistent source. In this way, you balance the use of cache and persistent medium. The following code demonstrates how to use a weak reference.

void SomeMethod()


  // Create a collection

  ArrayList arr = new ArrayList(5);


  // Create a custom object

  MyObject mo = new MyObject();


  // Create a WeakReference object from the custom object

  WeakReference wk = new WeakReference(mo);


  // Add the WeakReference object to the collection



  // Retrieve the weak reference

  WeakReference weakReference = (WeakReference)arr[0];

  MyObject mob = null;

  if( weakReference.IsAlive ) {

    mob = (MyOBject)weakReference.Target;


  if(mob==null) {

    // Resurrect the object as it has been garbage collected


  //continue because we have the object



Prevent the Promotion of Short-Lived Objects – Objects that are allocated and collected before leaving Gen 0 are referred as shortlived objects. The following principles help ensure that your short-lived objects are not promoted:

  • Do not reference short-lived objects from long-lived objects.
  • Avoid implementing a Finalize method. The garbage collector must promote finalizable objects to older generations to facilitate finalization, which makes them long-lived objects.
  • Avoid having finalizable objects refer to anything. This can cause the referenced object(s) to become long-lived.

Set Unneeded Member Variables to Null Before Making Long-Running Calls – Before you block on a long-running call, you should explicitly set any unneeded member variables to null before making the call so they can be collected. This is demonstrated in the following code fragment.

class MyClass{

  private string str1;

  private string str2;


  void DoSomeProcessing(…){

    str1= GetResult(…);

    str2= GetOtherResult(…);


  void MakeDBCall(…){




    // Make a database (long running) call



This applies to any objects which are still statically or lexically reachable but are actually not needed:

  • If you no longer need a static variable in your class, or some other class, set it to null.
  • If you can “prune” your state, that is also a good idea. You might be able to eliminate most of a tree before a long-running call, for instance.
  • If there are any objects that could be disposed before the long-running call, set those to null.

Do not set local variables to null (C#) or Nothing (Visual Basic .NET) because the JIT compiler can statically determine that the variable is no longer referenced and there is no need to explicitly set it to null.

Minimize Hidden Allocations – Memory allocation is extremely quick because it involves only a pointer relocation to create space for the new object. However, the memory has to be garbage collected at some point and that can hurt performance, so be aware of apparently simple lines of code that actually result in many allocations.

Also watch out for allocations that occur inside a loop such as string concatenations using the += operator. Finally, hashing methods and comparison methods are particularly bad places to put allocations because they are often called repeatedly in the context of searching and sorting. For more information about how to handle strings efficiently, see “String Operations” later in this chapter.

Avoid or Minimize Complex Object Graphs – Try to avoid using complex data structures or objects that contain a lot of references to other objects. These can be expensive to allocate and create additional work for the garbage collector. Simpler graphs have superior locality and less code is needed to maintain them. A common mistake is to make the graphs too general.

Avoid Preallocating and Chunking Memory – C++ programmers often allocate a large block of memory (using malloc) and then use chunks at a time, to save multiple calls to malloc. This is not advisable for managed code for several reasons:

  • Allocation of managed memory is a quick operation and the garbage collector has been optimized for extremely fast allocations. The main reason for preallocating memory in unmanaged code is to speed up the allocation process. This is not an issue for managed code.
  • If you preallocate memory, you cause more allocations than needed; this can trigger unnecessary garbage collections.
  • The garbage collector is unable to reclaim the memory that you manually recycle.
  • Preallocated memory ages and costs more to recycle when it is ultimately released.

The garbage collector provides two more additional methods; Finalize and Dispose. We’ll discuss the guidelines for these methods in the next article in this series.

Read the first three posts of this series: Part 1; Part2 ; Part 3 and let us know what you think!