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 automatic memory 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 automatically managing memory is known as garbage collection. While generally more complex for runtimes to implement than traditional manual memory management, garbage collection greatly simplifies development and eliminates many common errors related to manual memory 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.

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, marking all the objects it touches as reachable. Once this process is complete, all objects that have not been marked are considered to be garbage.

Objects that are considered garbage and that don’t have finalizers are immediately discarded, and the memory is reclaimed. Objects that are considered garbage and that 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.

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 Gen0 objects. 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 object is attempted.

Many other optimizations are also used to enhance the performance of automatic memory management, and in general a GC-based application can be expected to approach the performance of one using manual memory management.

Finalizers

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

Although this is required in certain cases, in general 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:

  • Objects with finalizers take longer to allocate on the managed heap than objects without finalizers.

  • Objects with finalizers that refer to other objects (even those without finalizers) can prolong the life of the referred objects unnecessarily.

  • It’s impossible to predict in what order the finalizers for a set of objects will be called.

  • You have limited control over when (or even if!) the finalizer for an object will be called.

In summary, finalizers are somewhat like lawyers: while there are cases where you really need them, in general 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:

  • Ensure that your finalizer executes quickly.

  • Never block in your finalizer.

  • Free any unmanaged resources you own.

  • Don’t reference any other objects.

  • Don’t throw any unhandled exceptions.

  • Call the Finalize method of the base class before returning.

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 Finalize method, include a special call to the static SuppressFinalize method on the System.GC type to indicate that the Finalize method no longer needs to be called. Typically the real Finalize method is written to call the Dispose/Close method, as follows:

public class Worker {
  ...
  public void Dispose( ) {
    // Perform normal cleanup
    ... 
    // Mark this object finalized
    GC.SuppressFinalize(this);
  }
  protected override void Finalize( ) {
    Dispose( );
    base.Finalize( );
  }
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.79.33