Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro CSharp And The .NET 2.0 Platform (2005) [eng]

.pdf
Скачиваний:
92
Добавлен:
16.08.2013
Размер:
10.35 Mб
Скачать

184 C H A P T E R 5 U N D E R S TA N D I N G O B J E C T L I F E T I M E

Understanding Object Generations

When the CLR is attempting to locate unreachable objects, is does not literally examine each and every object placed on the managed heap. Obviously, doing so would involve considerable time, especially in larger (i.e., real-world) applications.

To help optimize the process, each object on the heap is assigned to a specific “generation.” The idea behind generations is simple: The longer an object has existed on the heap, the more likely it is to stay there. For example, the object implementing Main() will be in memory until the program terminates. Conversely, objects that have been recently placed on the heap are likely to be unreachable rather quickly (such as an object created within a method scope). Given these assumptions, each object on the heap belongs to one of the following generations:

Generation 0: Identifies a newly allocated object that has never been marked for collection

Generation 1: Identifies an object that has survived a garbage collection (i.e., it was marked for collection, but was not removed due to the fact that the sufficient heap space was acquired)

Generation 2: Identifies an object that has survived more than one sweep of the garbage collector

The garbage collector will investigate all generation 0 objects first. If marking and sweeping these objects results in the required amount of free memory, any surviving objects are promoted to generation 1. To illustrate how an object’s generation affects the collection process, ponder Figure 5-5, which diagrams how a set of surviving generation 0 objects (A, B, and E) are promoted once the required memory has been reclaimed.

Figure 5-5. Generation 0 objects that survive a garbage collection are promoted to generation 1.

If all generation 0 objects have been evaluated, but additional memory is still required, generation 1 objects are then investigated for their “reachability” and collected accordingly. Surviving generation 1 objects are then promoted to generation 2. If the garbage collector still requires additional memory, generation 2 objects are then evaluated for their reachability. At this point, if a generation 2 object survives a garbage collection, it remains a generation 2 object given the predefined upper limit of object generations.

The bottom line is that by assigning a generational value to objects on the heap, newer objects (such as local variables) will be removed quickly, while older objects (such as a program’s application object) are not “bothered” as often.

C H A P T E R 5 U N D E R S TA N D I N G O B J E C T L I F E T I M E

185

The System.GC Type

The base class libraries provide a class type named System.GC that allows you to programmatically interact with the garbage collector using a set of static members. Now, do be very aware that you will seldom (if ever) need to make use of this type directly in your code. Typically speaking, the only time you will make use of the members of System.GC is when you are creating types that make use of unmanaged resources. Table 5-1 provides a rundown of some of the more interesting members (consult the .NET Framework 2.0 SDK Documentation for complete details).

Table 5-1. Select Members of the System.GC Type

System.GC Member

Meaning in Life

AddMemoryPressure(),

Allow you to specify a numerical value that represents the calling

RemoveMemoryPressure()

object’s “urgency level” regarding the garbage collection process. Be

 

aware that these methods should alter pressure in tandem and thus

 

never remove more pressure than the total amount you have added.

Collect()

Forces the GC to perform a garbage collection.

CollectionCount()

Returns a numerical value representing how many times a given

 

generation has been swept.

GetGeneration()

Returns the generation to which an object currently belongs.

GetTotalMemory()

Returns the estimated amount of memory (in bytes) currently allocated

 

on the managed heap. The Boolean parameter specifies whether the

 

call should wait for garbage collection to occur before returning.

MaxGeneration

Returns the maximum of generations supported on the target

 

system. Under Microsoft’s .NET 2.0, there are three possible

 

generations (0, 1, and 2).

SuppressFinalize()

Sets a flag indicating that the specified object should not have its

 

Finalize() method called.

WaitForPendingFinalizers()

Suspends the current thread until all finalizable objects have been

 

finalized. This method is typically called directly after invoking

 

GC.Collect().

 

 

Ponder the following Main() method, which illustrates select members of System.GC:

static void Main(string[] args)

{

//Print out estimated number of bytes on heap.

Console.WriteLine("Estimated bytes on heap: {0}", GC.GetTotalMemory(false));

//MaxGeneration is zero based, so add 1 for display purposes.

Console.WriteLine("This OS has {0} object generations.\n", (GC.MaxGeneration + 1));

Car refToMyCar = new Car("Zippy", 100);

Console.WriteLine(refToMyCar.ToString());

// Print out generation of refToMyCar object.

Console.WriteLine("Generation of refToMyCar is: {0}", GC.GetGeneration(refToMyCar));

Console.ReadLine();

}

186 C H A P T E R 5 U N D E R S TA N D I N G O B J E C T L I F E T I M E

Forcing a Garbage Collection

Again, the whole purpose of the .NET garbage collector is to manage memory on our behalf. However, under some very rare circumstances, it may be beneficial to programmatically force a garbage collection using GC.Collect(). Specifically:

Your application is about to enter into a block of code that you do not wish to be interrupted by a possible garbage collection.

Your application has just finished allocating an extremely large number of objects and you wish to remove as much of the acquired memory as possible.

If you determine it may be beneficial to have the garbage collector check for unreachable objects, you could explicitly trigger a garbage collection, as follows:

static void Main(string[] args)

{

...

//Force a garbage collection and wait for

//each object to be finalized.

GC.Collect();

GC.WaitForPendingFinalizers();

...

}

When you manually force a garbage collection, you should always make a call to GC.WaitForPendingFinalizers(). With this approach, you can rest assured that all finalizable objects have had a chance to perform any necessary cleanup before your program continues forward. Under the hood, GC.WaitForPendingFinalizers() will suspend the calling “thread” during the collection process.

This is a good thing, as it ensures your code does not invoke methods on an object currently being destroyed!

The GC.Collect() method can also be supplied a numerical value that identifies the oldest generation on which a garbage collection will be performed. For example, if you wished to instruct the CLR to only investigate generation 0 objects, you would write the following:

static void Main(string[] args)

{

...

// Only investigate generation 0 objects.

GC.Collect(0);

GC.WaitForPendingFinalizers();

...

}

Like any garbage collection, calling GC.Collect() will promote surviving generations. To illustrate, assume that our Main() method has been updated as follows:

static void Main(string[] args)

{

Console.WriteLine("***** Fun with System.GC *****\n");

//Print out estimated number of bytes on heap.

Console.WriteLine("Estimated bytes on heap: {0}", GC.GetTotalMemory(false));

//MaxGeneration is zero based.

Console.WriteLine("This OS has {0} object generations.\n", (GC.MaxGeneration + 1));

C H A P T E R 5 U N D E R S TA N D I N G O B J E C T L I F E T I M E

187

Car refToMyCar = new Car("Zippy", 100);

Console.WriteLine(refToMyCar.ToString());

//Print out generation of refToMyCar.

Console.WriteLine("\nGeneration of refToMyCar is: {0}", GC.GetGeneration(refToMyCar));

//Make a ton of objects for testing purposes. object[] tonsOfObjects = new object[50000]; for (int i = 0; i < 50000; i++)

tonsOfObjects[i] = new object();

//Collect only gen 0 objects.

GC.Collect(0);

GC.WaitForPendingFinalizers();

//Print out generation of refToMyCar.

Console.WriteLine("Generation of refToMyCar is: {0}", GC.GetGeneration(refToMyCar));

//See if tonsOfObjects[9000] is still alive.

if (tonsOfObjects[9000] != null)

{

Console.WriteLine("Generation of tonsOfObjects[9000] is: {0}", GC.GetGeneration(tonsOfObjects[9000]));

}

else

Console.WriteLine("tonsOfObjects[9000] is no longer alive.");

// Print out how many times a generation has been swept.

Console.WriteLine("\nGen 0 has been swept {0} times", GC.CollectionCount(0));

Console.WriteLine("Gen 1 has been swept {0} times", GC.CollectionCount(1));

Console.WriteLine("Gen 2 has been swept {0} times", GC.CollectionCount(2));

Console.ReadLine();

}

Here, we have purposely created a very large array of objects for testing purposes. As you can see from the output shown in Figure 5-6, even though this Main() method only made one explicit request for a garbage collection, the CLR performed a number of them in the background.

188 C H A P T E R 5 U N D E R S TA N D I N G O B J E C T L I F E T I M E

Figure 5-6. Interacting with the CLR garbage collector via System.GC

At this point in the chapter, I hope you feel more comfortable regarding the details of object lifetime. The remainder of this chapter examines the garbage collection process a bit further by addressing how you can build finalizable objects as well as disposable objects. Be very aware that the following techniques will only be useful if you are build managed classes that maintain internal unmanaged resources.

Source Code The SimpleGC project is included under the Chapter 5 subdirectory.

Building Finalizable Objects

In Chapter 3, you learned that the supreme base class of .NET, System.Object, defines a virtual method named Finalize(). The default implementation of this method does nothing whatsoever:

// System.Object public class Object

{

...

protected virtual void Finalize() {}

}

When you override Finalize() for your custom classes, you establish a specific location to perform any necessary cleanup logic for your type. Given that this member is defined as protected, it is not possible to directly call an object’s Finalize() method. Rather, the garbage collector will call an object’s Finalize() method (if supported) before removing the object from memory.

Of course, a call to Finalize() will (eventually) occur during a “natural” garbage collection or when you programmatically force a collection via GC.Collect(). In addition, a type’s finalizer method will automatically be called when the application domain hosting your application is unloaded from memory. Based on your current background in .NET, you may know that application domains (or simply AppDomains) are used to host an executable assembly and any necessary external code libraries. If you are not familiar with this .NET concept, you will be by the time you’ve finished Chapter 13. The short answer is that when your AppDomain is unloaded from memory, the CLR automatically invokes finalizers for every finalizable object created during its lifetime.

C H A P T E R 5 U N D E R S TA N D I N G O B J E C T L I F E T I M E

189

Now, despite what your developer instincts may tell you, a vast majority of your C# classes will not require any explicit cleanup logic. The reason is simple: If your types are simply making use of other managed objects, everything will eventually be garbage collected. The only time you would need to design a class that can clean up after itself is when you are making use of unmanaged resources (such as raw OS file handles, raw unmanaged database connections, or other unmanaged resources). As you may know, unmanaged resources are obtained by directly calling into the API of the operating system using PInvoke (platform invocation) services or due to some very elaborate COM interoperability scenarios. Given this, consider the next rule of garbage collection:

Rule: The only reason to override Finalize() is if your C# class is making use of unmanaged resources via PInvoke or complex COM interoperability tasks (typically via the System.Runtime. InteropServices.Marshal type).

Note Recall from Chapter 3 that it is illegal to override Finalize() on structure types. This makes perfect sense given that structures are value types, which are never allocated on the heap to begin with.

Overriding System.Object.Finalize()

In the rare case that you do build a C# class that makes use of unmanaged resources, you will obviously wish to ensure that the underlying memory is released in a predictable manner. Assume you have created a class named MyResourceWrapper that makes use of an unmanaged resource (whatever that may be) and you wish to override Finalize(). The odd thing about doing so in C# is that you cannot do so using the expected override keyword:

public class MyResourceWrapper

{

// Compile time error!

protected override void Finalize(){ }

}

Rather, when you wish to configure your custom C# class types to override the Finalize() method, you make use of the following (C++-like) destructor syntax to achieve the same effect. The reason for this alternative form of overriding a virtual method is that when the C# compiler processes a destructor, it will automatically add a good deal of required infrastructure within the Finalize() method (shown in just a moment).

Here is a custom finalizer for MyResourceWrapper that will issue a system beep when invoked. Obviously this is only for instructional purposes. A real-world finalizer would do nothing more than free any unmanaged resources and would not interact with the members of other managed objects, as you cannot assume they are still alive at the point the garbage collector invokes your Finalize() method:

// Override System.Object.Finalize() via destructor syntax. class MyResourceWrapper

{

~MyResourceWrapper()

{

//Clean up unmanaged resources here.

//Beep when destroyed (testing purposes only!)

Console.Beep();

}

}

190 C H A P T E R 5 U N D E R S TA N D I N G O B J E C T L I F E T I M E

If you were to examine this C# destructor using ilasm.exe, you will see that the compiler inserts some necessary error checking code. First, the code statements within the scope of your Finalize() method are placed within a try block. This bit of syntax is used to hold code statements that may trigger a runtime error (formally termed an exception) during their execution. The related finally block ensures that your base classes’ Finalize() method will always execute, regardless of any exceptions encountered within the try scope. You’ll investigate the formalities of structured exception handling in the next chapter; however, ponder the following CIL representation of MyResourceWrapper’s C# destructor:

.method family hidebysig virtual instance void Finalize() cil managed

{

// Code size

13

(0xd)

.maxstack 1

 

 

 

.try

 

 

 

{

 

 

 

IL_0000:

ldc.i4

 

0x4e20

IL_0005:

ldc.i4

 

0x3e8

IL_000a:

call

 

 

void [mscorlib]System.Console::Beep(int32, int32)

IL_000f:

nop

 

 

IL_0010:

nop

 

 

IL_0011:

leave.s

IL_001b

}// end .try finally

{

IL_0013: ldarg.0

IL_0014:

call instance void [mscorlib]System.Object::Finalize()

IL_0019: nop IL_001a: endfinally

}// end handler

IL_001b: nop IL_001c: ret

} // end of method MyResourceWrapper::Finalize

If you were to now test the MyResourceWrapper type, you would find that a system beep occurs when the application terminates, given that the CLR will automatically invoke finalizers upon AppDomain shutdown:

static void Main(string[] args)

{

Console.WriteLine("***** Fun with Finalizers *****\n");

Console.WriteLine("Hit the return key to shut down this app"); Console.WriteLine("and force the GC to invoke Finalize()"); Console.WriteLine("for finalizable objects created in this AppDomain."); Console.ReadLine();

MyResourceWrapper rw = new MyResourceWrapper();

}

Source Code The SimpleFinalize project is included under the Chapter 5 subdirectory.

C H A P T E R 5 U N D E R S TA N D I N G O B J E C T L I F E T I M E

191

Detailing the Finalization Process

Not to beat a dead horse, but always remember that the role of the Finalize() method is to ensure that a .NET object can clean up unmanaged resources when garbage collected. Thus, if you are building a type that does not make use of unmanaged entities (by far the most common case), finalization is of little use. In fact, if at all possible, you should design your types to avoid supporting a Finalize() method for the very simple reason that finalization takes time.

When you allocate an object onto the managed heap, the runtime automatically determines whether your object supports a custom Finalize() method. If so, the object is marked as finalizable, and a pointer to this object is stored on an internal queue named the finalization queue. The finalization queue is a table maintained by the garbage collector that points to each and every object that must be finalized before it is removed from the heap.

When the garbage collector determines it is time to free an object from memory, it examines each entry on the finalization queue, and copies the object off the heap to yet another managed structure termed the finalization reachable table (often abbreviated as freachable, and pronounced “eff-reachable”). At this point, a separate thread is spawned to invoke the Finalize() method for each object on the freachable table at the next garbage collection. Given this, it will take at very least two garbage collections to truly finalize an object.

The bottom line is that while finalization of an object does ensure an object can clean up unmanaged resources, it is still nondeterministic in nature, and due to the extra behind-the- curtains processing, considerably slower.

Building Disposable Objects

Given that so many unmanaged resources are “precious items” that should be cleaned up ASAP, allow me to introduce you to another possible technique used to handle an object’s cleanup. As an alternative to overriding Finalize(), your class could implement the IDisposable interface, which defines a single method named Dispose():

public interface IDisposable

{

void Dispose();

}

If you are new to interface-based programming, Chapter 7 will take you through the details. In a nutshell, an interface as a collection of abstract members a class or structure may support. When you do support the IDisposable interface, the assumption is that when the object user is finished using the object, it manually calls Dispose() before allowing the object reference to drop out of scope. In this way, your objects can perform any necessary cleanup of unmanaged resources without incurring the hit of being placed on the finalization queue and without waiting for the garbage collector to trigger the class’s finalization logic.

Note Structures and class types can both support IDisposable (unlike overriding Finalize(), which is reserved for class types).

Here is an updated MyResourceWrapper class that now implements IDisposable, rather than overriding System.Object.Finalize():

// Implementing IDisposable.

public class MyResourceWrapper : IDisposable

{

192C H A P T E R 5 U N D E R S TA N D I N G O B J E C T L I F E T I M E

//The object user should call this method

//when they finished with the object. public void Dispose()

{

//Clean up unmanaged resources here.

//Dispose other contained disposable objects.

}

}

Notice that a Dispose() method is not only responsible for releasing the type’s unmanaged resources, but should also call Dispose() on any other contained disposable methods. Unlike Finalize(), it is perfectly safe to communicate with other managed objects within a Dispose() method. The reason is simple: The garbage collector has no clue about the IDisposable interface and will never call Dispose(). Therefore, when the object user calls this method, the object is still living a productive life on the managed heap and has access to all other heap-allocated objects. The calling logic is straightforward:

public class Program

{

static void Main()

{

MyResourceWrapper rw = new MyResourceWrapper(); rw.Dispose();

Console.ReadLine();

}

}

Of course, before you attempt to call Dispose() on an object, you will want to ensure the type supports the IDisposable interface. While you will typically know which objects implement IDisposable by consulting the .NET Framework 2.0 SDK documentation, a programmatic check can be accomplished using the is or as keywords discussed in Chapter 4:

public class Program

{

static void Main()

{

MyResourceWrapper rw = new MyResourceWrapper(); if (rw is IDisposable)

rw.Dispose();

Console.ReadLine();

}

}

This example exposes yet another rule of working with garbage-collected types.

Rule: Always call Dispose() on any object you directly create if the object supports IDisposable. The assumption you should make is that if the class designer chose to support the Dispose() method, the type has some cleanup to perform.

Reusing the C# using Keyword

When you are handling a managed object that implements IDisposable, it will be quite common to make use of structured exception handling (again, see Chapter 6) to ensure the type’s Dispose() method is called in the event of a runtime exception:

C H A P T E R 5 U N D E R S TA N D I N G O B J E C T L I F E T I M E

193

static void Main(string[] args)

{

MyResourceWrapper rw = new MyResourceWrapper (); try

{

// Use the members of rw.

}

finally

{

// Always call Dispose(), error or not. rw.Dispose();

}

}

While this is a fine example of defensive programming, the truth of the matter is that few developers are thrilled by the prospects of wrapping each and every disposable type within

a try/catch/finally block just to ensure the Dispose() method is called. To achieve the same result in a much less obtrusive manner, C# supports a special bit of syntax that looks like this:

static void Main(string[] args)

{

//Dispose() is called automatically when the

//using scope exits.

using(MyResourceWrapper rw = new MyResourceWrapper())

{

// Use rw object.

}

}

If you were to look at the CIL code of the Main() method using ildasm.exe, you will find the using syntax does indeed expand to try/final logic, with the expected call to Dispose():

.method private hidebysig static void Main(string[] args) cil managed

{

...

.try

{

...

} // end .try finally

{

...

IL_0012: callvirt instance void

SimpleFinalize.MyResourceWrapper::Dispose()

}// end handler

...

} // end of method Program::Main

Note If you attempt to “use” an object that does not implement IDisposable, you will receive a compiler error.

While this syntax does remove the need to manually wrap disposable objects within try/finally logic, the C# using keyword unfortunately now has a double meaning (specifying namespaces and invoking a Dispose() method). Nevertheless, when you are working with .NET types that support the IDisposable interface, this syntactical construct will ensure that the object “being used” will automatically have its Dispose() method called once the using block has exited.