only for RuBoard - do not distribute or recompile Previous Section Next Section

3.12 Automatic Memory Management

Almost all modern programming languages allocate memory in two places: on the stack and on the heap.

Memory allocated on the stack stores local variables, parameters, and return values, and is generally managed automatically by the operating system.

Memory allocated on the heap, however, is treated differently by different languages. In C and C++, memory allocated on the heap is managed manually. In C# and Java, however, memory allocated on the heap is managed automatically.

While manual memory management has the advantage of being simple for runtimes to implement, it has drawbacks that tend not to exist in systems that offer automaticmemory management. For example, a large percentage of bugs in C and C++ programs stem from using an object after it has been deleted (dangling pointers) or from forgetting to delete an object when it is no longer needed (memory leaks).

The process of automaticallymanaging memory is known as garbage collection. While generally more complex for runtimes to implement than traditional manualmemory management, garbage collection greatly simplifies development and eliminates many common errors related to manualmemory management.

For example, it is almost impossible to generate a traditional memory leak in C#, and common bugs such as circular references in traditional COM development simply go away.

3.12.1 The Garbage Collector

C# depends on the CLR for many of its runtime services, and garbage collection is no exception.

The CLR includes a high-performing generational mark-and-compact garbage collector (GC)that performs automatic memory management for type instances stored on the managed heap.

The GC is considered to be a tracing garbage collector in that it doesn't interfere with every access to an object, but rather wakes up intermittently and traces the graph of objects stored on the managed heap to determine which objects can be considered garbage and therefore collected.

The GC generally initiates a garbage collection when a memory allocation occurs, and memory is too low to fulfill the request. This process can also be initiated manually using the System.GC type. Initiating a garbage collection freezes all threads in the process to allow the GC time to examine the managed heap.

The GC begins with the set of object references considered roots and walks the object graph, markingall the objects it touches as reachable. Once this process is complete, all objects that have not been marked are considered garbage.

Objects that are considered garbage and don't have finalizers are immediately discarded, and the memory is reclaimed. Objects that are considered garbage and do have finalizers are flagged for additional asynchronous processing on a separate thread to invoke their Finalize methods before they can be considered garbage and reclaimed at the next collection.

Objects considered still live are then shifted down to the bottom of the heap (compacted ), hopefully freeing space to allow the memory allocation to succeed.

At this point the memory allocation is attempted again, the threads in the process are unfrozen, and either normal processing continues or an OutOfMemoryException is thrown.

3.12.2 Optimization Techniques

Although this may sound like an inefficient process compared to simply managing memory manually, the GC incorporates various optimization techniques to reduce the time an application is frozen waiting for the GC to complete (known as pause time).

The most important of these optimizations is what makes the GC generational. This techniques takes advantage of the fact that while many objects tend to be allocated and discarded rapidly, certain objects are long-lived and thus don't need to be traced during every collection.

Basically, the GC divides the managed heap into three generations. Objects that have just been allocated are considered to be in Gen0, objects that have survived one collection cycle are considered to be in Gen1, and all other objects are considered to be in Gen2.

When it performs a collection, the GC initially collects only Gen0objects. If not enough memory is reclaimed to fulfill the request, both Gen0 and Gen1 objects are collected, and if that fails as well, a full collection of Gen0, Gen1, and Gen2 objects is attempted.

Many other optimizations are also used to enhance the performance of automatic memory management, and generally, a GC-based application can be expected to approach the performance of an application that uses manual memory management.

3.12.3 Finalizers

When implementing your own types, you can choose to give them finalizers (via C# destructors), which are methods called asynchronously by the GC once an object is determined to be garbage.

Although this is required in certain cases, generally, there are many good technical reasons to avoid the use of finalizers.

As described in the previous section, objects with finalizers incur significant overhead when they are collected, requiring asynchronous invocation of their Finalize methods and taking two full GC cycles for their memory to be reclaimed.

Other reasons not to use finalizers include:

In summary, finalizers are somewhat like lawyers: while there are cases when you really need them, generally, you don't want to use them unless absolutely necessary, and if you do use them, you need to be 100% sure you understand what they are doing for you.

If you have to implement a finalizer, follow these guidelines or have a very good reason for not doing so:

3.12.4 Dispose and Close Methods

It is generally desirable to explicitly call clean-up code once you have determined that an object will no longer be used. Microsoft recommends that you write a method named either Dispose or Close (depending on the semantics of the type) to perform the cleanup required. If you also have a destructor, include a special call to the static SuppressFinalize method on the System.GC type to indicate that the destructor no longer needs to be called. Typically, the real destructoris written to call the Dispose/Close method, as follows:

using System;
public class Worker : IDisposable {
  bool disposed = false;
  int id;
  public Worker(int id) {
    this.id=id;
  }
  // ...
  protected virtual void Dispose(bool disposing) {
    if (!this.disposed) { // don't dispose more than once
      if (disposing) {
        // disposing==true means you're not in the finalizer, so 
        // you can reference other objects here
        Console.WriteLine("#{0}: OK to clean up other objects.", id);
      }
      Console.WriteLine("#{0}: disposing.", id);
      // Perform normal cleanup
    }
    this.disposed = true;
  }
  public void Dispose(  ) {
    Dispose(true); 
    // Mark this object finalized
    GC.SuppressFinalize(this); // no need to destruct this instance 
  }
  ~Worker(  ) {
    Dispose(  false);
    Console.WriteLine("#{0}: destructing.", id);
  }
  public static void Main(  ) {
    // create a worker and call Dispose when we're done.
    using(Worker w1 = new Worker(1)) {
      // ...
    }
    // create a worker that will get cleaned up when the CLR
    // gets around to it.
    Worker w2 = new Worker(2);
  }
}

If you run this code, you will see that Worker 1 is never finalized, since its Dispose( ) method is implicitly called (the using block guarantees this). Worker 2 is finalized and disposed when the CLR gets around to it, but it's never given a chance to clean up other objects. The disposable pattern gives you a way to close or dispose of any external objects you might be using, such as an I/O stream:

#1: OK to clean up other objects.
#1: disposing.
#2: disposing.
#2: destructing.
only for RuBoard - do not distribute or recompile Previous Section Next Section