Tải bản đầy đủ - 0 (trang)
Background Garbage Collection Under .NET 4.0 and Greater

Background Garbage Collection Under .NET 4.0 and Greater

Tải bản đầy đủ - 0trang

CHAPTER 13  UNDERSTANDING OBJECT LIFETIME



System.GC Member



Description



GetTotalMemory()



Returns the estimated amount of memory (in bytes) currently

allocated on the managed heap. A Boolean parameter specifies

whether the call should wait for garbage collection to occur

before returning.



MaxGeneration



Returns the maximum number of generations supported on the

target system. Under Microsoft’s .NET 4.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().



To illustrate how the System.GC type can be used to obtain various garbage collection–centric

details, consider the following Main() method, which makes use of several members of GC:

static void Main(string[] args)

{

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

// 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();

}



Forcing a Garbage Collection

Again, the whole purpose of the .NET garbage collector is to manage memory on our behalf. However, in

some very rare circumstances, it may be beneficial to programmatically force a garbage collection using

GC.Collect(). Here are two common situations where you might consider interacting with the collection

process:



482

www.it-ebooks.info



CHAPTER 13  UNDERSTANDING OBJECT LIFETIME







Your application is about to enter into a block of code that you don’t want

interrupted by a possible garbage collection.







Your application has just finished allocating an extremely large number of objects

and you want to remove as much of the acquired memory as soon as possible.



If you determine it could 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

(described in the next section) have had a chance to perform any necessary cleanup before your

program continues. 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, to instruct the CLR to

investigate only generation 0 objects, you would write the following:

static void Main(string[] args)

{

...

// Only investigate generation 0 objects.

GC.Collect(0);

GC.WaitForPendingFinalizers();

...

}

As well, the Collect() method can also be passed in a value of the GCCollectionMode enumeration as

a second parameter, to fine-tune exactly how the runtime should force the garbage collection. This enum

defines the following values:

public enum GCCollectionMode

{

Default,

// Forced is the current default.

Forced,

// Tells the runtime to collect immediately!

Optimized

// Allows the runtime to determine

// whether the current time is optimal to reclaim objects.

}

As with any garbage collection, calling GC.Collect()promotes surviving generations. To illustrate,

assume that our Main() method has been updated as follows:



483

www.it-ebooks.info



CHAPTER 13  UNDERSTANDING OBJECT LIFETIME



static void Main(string[] args)

{

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

// 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));

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, GCCollectionMode.Forced);

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 object types (50,000 to be exact) for testing

purposes. As you can see from the output that follows, even though this Main() method made only one



484

www.it-ebooks.info



CHAPTER 13  UNDERSTANDING OBJECT LIFETIME



explicit request for a garbage collection (via the GC.Collect() method), the CLR performed a number of

them in the background.

***** Fun with System.GC *****

Estimated bytes on heap: 70240

This OS has 3 object generations.

Zippy is going 100 MPH

Generation of refToMyCar is: 0

Generation of refToMyCar is: 1

Generation of tonsOfObjects[9000] is: 1

Gen 0 has been swept 1 times

Gen 1 has been swept 0 times

Gen 2 has been swept 0 times

At this point, I hope you feel more comfortable regarding the details of object lifetime. In the next

section, we’ll examine 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 are typically

necessary only if you are building C# classes that maintain internal unmanaged resources.



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



Building Finalizable Objects

In Chapter 6, 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 from a class instance via the dot operator. Rather,

the garbage collector will call an object’s Finalize() method (if supported) before removing the object

from memory.



485

www.it-ebooks.info



CHAPTER 13  UNDERSTANDING OBJECT LIFETIME



 Note 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 and, therefore, are not garbage collected!

However, if you create a structure that contains unmanaged resources that need to be cleaned up, you can

implement the IDisposable interface (described shortly).



Of course, a call to Finalize() will (eventually) occur during a “natural” garbage collection or

possibly 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. Depending on your 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 17. For

now, note that when your AppDomain is unloaded from memory, the CLR automatically invokes

finalizers for every finalizable object created during its lifetime.

Now, despite what your developer instincts may tell you, the vast majority of your C# classes will not

require any explicit cleanup logic or a custom finalizer. The reason is simple: if your classes are just

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 using unmanaged

resources (such as raw OS file handles, raw unmanaged database connections, chunks of unmanaged

memory, or other unmanaged resources). Under the .NET platform, unmanaged resources are obtained

by directly calling into the API of the operating system using Platform Invocation Services (PInvoke) or

as a result of some very elaborate COM interoperability scenarios. Given this, consider the next rule of

garbage collection:



 Rule The only compelling reason to override Finalize() is if your C# class is making use of unmanaged

resources via PInvoke or complex COM interoperability tasks (typically via various members defined by the

System.Runtime.InteropServices.Marshal type). The reason is that under these scenarios, you are

manipulating memory that the CLR cannot manage.



Overriding System.Object.Finalize()

In the rare case that you do build a C# class that uses unmanaged resources, you will obviously want to

ensure that the underlying memory is released in a predictable manner. Suppose you have created a new

C# Console Application named SimpleFinalize and inserted a class named MyResourceWrapper that uses

an unmanaged resource (whatever that might be) and you want to override Finalize(). The odd thing

about doing so in C# is that you can’t do it using the expected override keyword.

class MyResourceWrapper

{

// Compile-time error!

protected override void Finalize(){ }

}



486

www.it-ebooks.info



CHAPTER 13  UNDERSTANDING OBJECT LIFETIME



Rather, when you want to configure your custom C# class types to override the Finalize() method,

you make use of a (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 the finalizer syntax, it

automatically adds a good deal of required infrastructure within the implicitly overridden Finalize()

method (shown in just a moment).

C# finalizers look very similar to constructors in that they are named identically to the class they are

defined within. In addition, finalizers are prefixed with a tilde symbol (~). Unlike a constructor, however,

a finalizer never takes an access modifier (they are implicitly protected), never takes parameters, and

can’t be overloaded (only one finalizer per class).

Following is a custom finalizer for MyResourceWrapper that will issue a system beep when invoked.

Obviously, this example is only for instructional purposes. A real-world finalizer would do nothing more

than free any unmanaged resources and would not interact with other managed objects, even those

referenced by the current object, as you can’t assume they are still alive at the point the garbage collector

invokes your Finalize() method.

// Override System.Object.Finalize() via finalizer syntax.

class MyResourceWrapper

{

~MyResourceWrapper()

{

// Clean up unmanaged resources here.



}



// Beep when destroyed (testing purposes only!)

Console.Beep();



}

If you were to examine this C# destructor using ildasm.exe, you would 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 (see Chapter 7). The related finally block ensures that your base

classes’ Finalize() method will always execute, regardless of any exceptions encountered within the try

scope.

.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:



487

www.it-ebooks.info



CHAPTER 13  UNDERSTANDING OBJECT LIFETIME



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 then tested 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 13 subdirectory.



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 it is garbage-collected. Thus, if you are building a

class that does not make use of unmanaged memory (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 “effreachable”). 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 the 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.



488

www.it-ebooks.info



CHAPTER 13  UNDERSTANDING OBJECT LIFETIME



Building Disposable Objects

As you have seen, finalizers can be used to release unmanaged resources when the garbage collector

kicks in. However, given that many unmanaged objects are “precious items” (such as raw database or

file handles), it could be valuable to release them as soon as possible instead of relying on a garbage

collection to occur. As an alternative to overriding Finalize(), your class could implement the

IDisposable interface, which defines a single method named Dispose() as follows:

public interface IDisposable

{

void Dispose();

}

When you do implement the IDisposable interface, the assumption is that when the object user is

finished using the object, the object user manually calls Dispose() before allowing the object reference

to drop out of scope. In this way, an object 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 implement IDisposable (unlike overriding Finalize(), which is

reserved for class types), as the object user (not the garbage collector) invokes the Dispose() method.



To illustrate the use of this interface, create a new C# Console Application named SimpleDispose.

Here is an updated MyResourceWrapper class that now implements IDisposable, rather than overriding

System.Object.Finalize():

// Implementing IDisposable.

class MyResourceWrapper : IDisposable

{

// The object user should call this method

// when they finish with the object.

public void Dispose()

{

// Clean up unmanaged resources...

// Dispose other contained disposable objects...



}



}



// Just for a test.

Console.WriteLine("***** In Dispose! *****");



Notice that a Dispose() method is not only responsible for releasing the type’s unmanaged

resources, but can also call Dispose() on any other contained disposable methods. Unlike with

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



489

www.it-ebooks.info



CHAPTER 13  UNDERSTANDING OBJECT LIFETIME



on the managed heap and has access to all other heap-allocated objects. The calling logic, shown here, is

straightforward:

class Program

{

static void Main(string[] args)

{

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

// Create a disposable object and call Dispose()

// to free any internal resources.

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 base class library types

implement IDisposable by consulting the .NET Framework 4.5 SDK documentation, a programmatic

check can be accomplished using the is or as keywords discussed in Chapter 6.

class Program

{

static void Main(string[] args)

{

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

MyResourceWrapper rw = new MyResourceWrapper();

if (rw is IDisposable)

rw.Dispose();

Console.ReadLine();

}

}

This example exposes yet another rule regarding memory management:



 Rule It is a good idea to 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. If you forget, memory will eventually be cleaned up (so don’t

panic), but it could take longer than necessary.



There is one caveat to the previous rule. A number of types in the base class libraries that do

implement the IDisposable interface provide a (somewhat confusing) alias to the Dispose() method, in

an attempt to make the disposal-centric method sound more natural for the defining type. By way of an

example, while the System.IO.FileStream class implements IDisposable (and therefore supports a

Dispose() method), it also defines the following Close() method that is used for the same purpose:



490

www.it-ebooks.info



CHAPTER 13  UNDERSTANDING OBJECT LIFETIME



// Assume you have imported

// the System.IO namespace...

static void DisposeFileStream()

{

FileStream fs = new FileStream("myFile.txt", FileMode.OpenOrCreate);



}



// Confusing, to say the least!

// These method calls do the same thing!

fs.Close();

fs.Dispose();



While it does feel more natural to “close” a file rather than “dispose” of one, this doubling up of

cleanup methods can be confusing. For the few types that do provide an alias, just remember that if a

type implements IDisposable, calling Dispose() is always a safe course of action.



Reusing the C# using Keyword

When you are handling a managed object that implements IDisposable, it is quite common to make use

of structured exception-handling to ensure the type’s Dispose() method is called in the event of a

runtime exception, like so:

static void Main(string[] args)

{

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

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/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)

{

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

// Dispose() is called automatically when the

// using scope exits.

using(MyResourceWrapper rw = new MyResourceWrapper())

{

// Use rw object.

}

}



491

www.it-ebooks.info



CHAPTER 13  UNDERSTANDING OBJECT LIFETIME



If you looked at the following CIL code of the Main() method using ildasm.exe, you would find the

using syntax does indeed expand to try/finally 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 (importing 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.

Also, be aware that it is possible to declare multiple objects of the same type within a using scope. As

you would expect, the compiler will inject code to call Dispose() on each declared object.

static void Main(string[] args)

{

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



}



// Use a comma-delimited list to declare multiple objects to dispose.

using(MyResourceWrapper rw = new MyResourceWrapper(),

rw2 = new MyResourceWrapper())

{

// Use rw and rw2 objects.

}



 Source Code The SimpleDispose project is included under the Chapter 13 subdirectory.



492

www.it-ebooks.info



Tài liệu bạn tìm kiếm đã sẵn sàng tải về

Background Garbage Collection Under .NET 4.0 and Greater

Tải bản đầy đủ ngay(0 tr)

×