The Life and Times of an Object


The Life and Times of an Object

First, let's recap what happens when you create and destroy an object.

You create an object like this:

TextBox message = new TextBox(); // TextBox is a reference type

From your point of view, the new operation is atomic, but underneath, object creation is really a two-phase process. First, the new operation has to allocate some raw memory from the heap. You have no control over this phase of an object's creation. Second, the new operation has to convert the raw memory into an object; it has to initialize the object. You can control this phase this by using a constructor.

NOTE
C++ programmers should note that in C#, you cannot overload new to control allocation.

When you have created an object, you can then access its members by using the dot operator. For example:

message.Text = "People of Earth, your attention please";

You can make other reference variables refer to the same object:

TextBox ref = message;

How many references can you create to an object? As many as you want! The runtime has to keep track of all these references. If the variable message disappears (by going out of scope), other variables (such as ref) might still exist. Therefore, the lifetime of an object cannot be tied to a particular reference variable. An object can only be destroyed when all the references to it have disappeared.

NOTE
C++ programmers should note that C# does not have a delete operator. The runtime controls when an object is destroyed.

Like object creation, object destruction is also a two-phase process. The two phases of destruction exactly mirror the two phases of creation. First, you have to perform some tidying up. You do this by writing a destructor. Second, the raw memory has to be given back to the heap; the memory that the object lived in has to be deallocated. Once again, you have no control over this phase. The process of destroying an object and returning memory back to the heap is known as garbage collection.

Writing Destructors

You can use a destructor to perform any tidying up required when an object is garbage collected. The syntax for writing a destructor is a tilde (~) followed by the name of the class. For example, here's a simple class that counts the number of live instances by incrementing a static count in the constructor and decrementing a static count in the destructor:

class Tally {     public Tally()     {         this.instanceCount++;     }     ~Tally()     {         this.instanceCount--;     }     public static int InstanceCount()     {         return this.instanceCount;     }     ...     private static int instanceCount = 0; }

There are some very important restrictions that apply to destructors:

  • You cannot declare a destructor in a struct. A struct is a value type that lives on the stack and not the heap, so garbage collection does not apply.

    struct Tally {     ~Tally() { ... } // compile-time error }

  • You cannot declare an access modifier (such as public) for a destructor. This is because you never call the destructor yourself—the garbage collector does.

    public ~Tally() { ... } // compile-time error

  • You never declare a destructor with parameters, and it cannot take any parameters. Again, this is because you never call the destructor yourself.

    ~Tally(int parameter) { ... } // compile-time error

  • The compiler automatically translates a destructor into an override of the Object.Finalize method. The compiler translates the following destructor:

    class Tally {     ~Tally() { ... } }

    Into this:

    class Tally {     protected override void Finalize()      {         try { ... }         finally { base.Finalize(); }     } }

    The compiler-generated Finalize method contains the destructor body inside a try block, followed by a finally block that calls the base class Finalize. (The try and finally keywords were described in Chapter 6, “Managing Errors and Exceptions.”) This ensures that a destructor always calls its base class destructor. It's important to realize that only the compiler can make this translation. You can't override Finalize yourself and you can't call Finalize yourself.

Why Use the Garbage Collector?

In C#, you can never destroy an object yourself. There just isn't any syntax to do it, and there are good reasons why the designers of C# decided to forbid you from doing it. If it was your responsibility to destroy objects, sooner or later one of the following situations would arise:

  • You'd forget to destroy the object. This would mean that the object's destructor (if it had one) would not be run, tidying up would not occur, and memory would not be deallocated back to the heap. You could quite easily run out of memory.

  • You'd try to destroy an active object. Remember, objects are accessed by reference. If a class held a reference to a destroyed object, it would be a dangling reference. The dangling reference would end up referring either to unused memory or possibly to a completely different object in the same piece of memory. Either way, the outcome of using dangling reference would be undefined. All bets would be off.

  • You'd try and destroy the same object more than once. This might or might not be disastrous, depending on the code in the destructor.

These problems are unacceptable in a language like C#, which places robustness and security high on its list of design goals. Instead, the garbage collector is responsible for destroying objects for you. The garbage collector guarantees the following:

  • Each object will be destroyed and its destructors run. When a program ends, all oustanding objects will be destroyed.

  • Each object is destroyed exactly once.

  • Each object is destroyed only when it becomes unreachable; that is, when no references refer to the object.

These guarantees are tremendously useful and free you, the programmer, from tedious housekeeping chores that are easy to get wrong. They allow you to concentrate on the logic of the program itself and be more productive.

When does garbage collection occur? This might seem like a strange question. After all, surely garbage collection occurs when an object is no longer needed. Well, it does, but not necessarily immediately. Garbage collection can be an expensive process, so the runtime collects garbage only when it needs to (when it thinks available memory is starting to run low), and then it collects as much as it can. Performing a few large sweeps of memory is more efficient than performing lots of little dustings!

NOTE
You can invoke the garbage collector in a program by calling the static method System.GC.Collect(). However, except in a few cases, this is not recommended. The System.GC.Collect method starts the garbage collector, but the process runs asynchronously and when the method call finishes you still don't know whether your objects have been destroyed. Let the runtime decide when it is best to collect garbage!

One feature of the garbage collector is that you don't know, and should not rely upon, the order in which objects will be destroyed. The final point to understand is arguably the most important: Destructors do not run until objects are garbage collected. If you write a destructor, you know it will be executed, you just don't know when.

How Does the Garbage Collector Work?

The garbage collector runs in its own thread and can execute only at certain times (typically when your application reaches the end of a method). While it runs, other threads running in your application will temporarily halt. This is because the garbage collector may need to move objects around and update object references; it cannot do this while objects are in use. The steps that the garbage collector takes are as follows:

  1. It builds a map of all reachable objects. It does this by repeatedly following reference fields inside objects. The garbage collector builds this map very carefully and makes sure that circular references do not cause an infinite recursion. Any object not in this map is deemed to be unreachable.

  2. It checks whether any of the unreachable objects has a destructor that needs to be run (a process called finalization). Any unreachable object that requires finalization is placed in a special queue called the freachable queue (pronounced F-reachable).

  3. It deallocates the remaining unreachable objects (those that don't require finalization) by moving the reachable objects down the heap, thus defragmenting the heap and freeing memory at the top of the heap. When the garbage collector moves a reachable object, it also updates any references to the object.

  4. At this point, it allows other threads to resume.

  5. It finalizes the unreachable objects that require finalization (now in the freachable queue) in a separate thread.

Recommendations

Writing classes that contain destructors adds complexity to your code and to the garbage collection process, and makes your program run more slowly. If your program does not contain any destructors, the garbage collector does not need to perform Steps 3 and 5 in the previous section. Clearly, not doing something is faster than doing it. Therefore, try to avoid using destructors except when you really need them. For example, consider a using statement instead (see the section titled “The using Statement” later in this chapter).

You need to write a destructor very carefully. In particular, you need to be aware that, if your destructor calls other objects, those other objects might have already had their destructor called by the garbage collector. Remember that the order of finalization is not guaranteed. Therefore, ensure that destructors do not depend on each other, or overlap with each other (don't have two destructors that try to release the same resource, for example).




Microsoft Visual C# 2005 Step by Step
Microsoft® Visual C#® 2005 Step by Step (Step By Step (Microsoft))
ISBN: B002CKYPPM
EAN: N/A
Year: 2005
Pages: 183
Authors: John Sharp

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net