Pro C#10 CHAPTER 9 Understanding Object Lifetime

CHAPTER 9

Understanding Object Lifetime

At this point in the book, you have learned a great deal about how to build custom class types using C#. Now you will see how the runtime manages allocated class instances (aka objects) via garbage collection (GC). C# programmers never directly deallocate a managed object from memory (recall there is no delete keyword in the C# language). Rather, .NET Core objects are allocated to a region of memory termed the managed heap, where they will be automatically destroyed by the garbage collector “sometime in the future.”
After you have looked at the core details of the collection process, you’ll learn how to programmatically interact with the garbage collector using the System.GC class type (which is something you will typically
not be required to do for a majority of your projects). Next, you’ll examine how the virtual System.Object. Finalize() method and IDisposable interface can be used to build classes that release internal unmanaged resources in a predictable and timely manner.
You will also delve into some functionality of the garbage collector introduced in .NET 4.0, including background garbage collections and lazy instantiation using the generic System.Lazy<> class. By the time you have completed this chapter, you will have a solid understanding of how .NET Core objects are managed by the runtime.

Classes, Objects, and References
To frame the topics covered in this chapter, it is important to further clarify the distinction between classes, objects, and reference variables. Recall that a class is nothing more than a blueprint that describes how an instance of this type will look and feel in memory. Classes, of course, are defined within a code file (which in C# takes a *.cs extension by convention). Consider the following simple Car class defined within a new C# Console Application project named SimpleGC:

namespace SimpleGC;
// Car.cs
public class Car
{
public int CurrentSpeed {get; set;} public string PetName {get; set;}

public Car(){}
public Car(string name, int speed)
{
PetName = name;
CurrentSpeed = speed;
}

© Andrew Troelsen, Phil Japikse 2022
A. Troelsen and P. Japikse, Pro C# 10 with .NET 6, https://doi.org/10.1007/978-1-4842-7869-7_9

357

public override string ToString()
=> $"{PetName} is going {CurrentSpeed} MPH";

}

After a class has been defined, you may allocate any number of objects using the C# new keyword. Understand, however, that the new keyword returns a reference to the object on the heap, not the actual object. If you declare the reference variable as a local variable in a method scope, it is stored on the stack for further use in your application. When you want to invoke members on the object, apply the C# dot operator to the stored reference, like so:

using SimpleGC;
Console.WriteLine(" GC Basics ");

// Create a new Car object on the managed heap.
// We are returned a reference to the object
// ("refToMyCar").
Car refToMyCar = new Car("Zippy", 50);

// The C# dot operator (.) is used to invoke members
// on the object using our reference variable. Console.WriteLine(refToMyCar.ToString()); Console.ReadLine();

Figure 9-1 illustrates the class, object, and reference relationship.

Figure 9-1. References to objects on the managed heap

■ Note Recall from Chapter 4 that structures are value types that are always allocated directly on the stack and are never placed on the .NET Core managed heap. Heap allocation occurs only when you are creating instances of classes.

The Basics of Object Lifetime
When you are building your C# applications, you are correct to assume that the .NET Core runtime environment will take care of the managed heap without your direct intervention. In fact, the golden rule of
.NET Core memory management is simple.

■ Rule allocate a class instance onto the managed heap using the new keyword and forget about it.

Once instantiated, the garbage collector will destroy an object when it is no longer needed. The next obvious question, of course, is “How does the garbage collector determine when an object is no longer needed?” The short (i.e., incomplete) answer is that the garbage collector removes an object from the heap only if it is unreachable by any part of your code base. Assume you have a method in your Program.cs file that allocates a local Car object as follows:

static void MakeACar()
{
// If myCar is the only reference to the Car object, it may be destroyed when this method returns.
Car myCar = new Car();
}

Notice that this Car reference (myCar) has been created directly within the MakeACar() method and has not been passed outside of the defining scope (via a return value or ref/out parameters). Thus, once this method call completes, the myCar reference is no longer reachable, and the associated Car object is now
a candidate for garbage collection. Understand, however, that you can’t guarantee that this object will be reclaimed from memory immediately after MakeACar() has completed. All you can assume at this point is that when the runtime performs the next garbage collection, the myCar object could be safely destroyed.
As you will most certainly discover, programming in a garbage-collected environment greatly simplifies your application development. In stark contrast, C++ programmers are painfully aware that if they fail
to manually delete heap-allocated objects, memory leaks are never far behind. In fact, tracking down memory leaks is one of the most time-consuming (and tedious) aspects of programming in unmanaged environments. By allowing the garbage collector to take charge of destroying objects, the burden of memory management has been lifted from your shoulders and placed onto those of the runtime.

The CIL of new
When the C# compiler encounters the new keyword, it emits a CIL newobj instruction into the method implementation. If you compile the current example code and investigate the resulting assembly using ildasm.exe, you’d find the following CIL statements within the MakeACar() method:

.method assembly hidebysig static
void ‘<

$>g MakeACar|0_0′() cil managed
{
// Code size 8 (0x8)
.maxstack 1
.locals init (class SimpleGC.Car V_0) IL_0000: nop

IL_0001: newobj instance void SimpleGC.Car::.ctor() IL_0006: stloc.0
IL_0007: ret
} // end of method ‘$’::'<

$>g MakeACar|0_0′

Before you examine the exact rules that determine when an object is removed from the managed heap, let’s check out the role of the CIL newobj instruction in a bit more detail. First, understand that the managed heap is more than just a random chunk of memory accessed by the runtime. The .NET Core garbage collector is quite a tidy housekeeper of the heap, given that it will compact empty blocks of memory (when necessary) for the purposes of optimization.
To aid in this endeavor, the managed heap maintains a pointer (commonly referred to as the next object pointer or new object pointer) that identifies exactly where the next object will be located. That said, the newobj instruction tells the runtime to perform the following core operations:
1.Calculate the total amount of memory required for the object to be allocated (including the memory required by the data members and the base classes).
2.Examine the managed heap to ensure that there is indeed enough room to host the object to be allocated. If there is, the specified constructor is called, and the caller is ultimately returned a reference to the new object in memory, whose address just happens to be identical to the last position of the next object pointer.
3.Finally, before returning the reference to the caller, advance the next object pointer to point to the next available slot on the managed heap.
Figure 9-2 illustrates the basic process.

Figure 9-2. The details of allocating objects onto the managed heap

As your application is busy allocating objects, the space on the managed heap may eventually become full. When processing the newobj instruction, if the runtime determines that the managed heap does not have sufficient memory to allocate the requested type, it will perform a garbage collection in an attempt to free up memory. Thus, the next rule of garbage collection is also quite simple.

■ Rule if the managed heap does not have sufficient memory to allocate a requested object, a garbage collection will occur.

Exactly how this garbage collection occurs, however, depends on which type of garbage collection your application uses. You’ll look at the differences a bit later in this chapter.

Setting Object References to null
C/C++ programmers often set pointer variables to null to ensure they are no longer referencing unmanaged memory. Given this, you might wonder what the end result is of assigning object references to null under C#. For example, assume the MakeACar() subroutine has now been updated as follows:

static void MakeACar()
{
Car myCar = new Car(); myCar = null;
}

When you assign object references to null, the compiler generates CIL code that ensures the reference (myCar, in this example) no longer points to any object. If you once again made use of ildasm.exe to view the CIL code of the modified MakeACar(), you would find the ldnull opcode (which pushes a null value on the virtual execution stack) followed by a stloc.0 opcode (which sets the null reference on the variable).

.method assembly hidebysig static
void ‘<

$>g MakeACar|0_0′() cil managed
{
// Code size 10 (0xa)
.maxstack 1
.locals init (class SimpleGC.Car V_0) IL_0000: nop
IL_0001: newobj instance void SimpleGC.Car::.ctor() IL_0006: stloc.0
IL_0007: ldnull IL_0008: stloc.0 IL_0009: ret
} // end of method ‘$’::'<
$>g MakeACar|0_0′

What you must understand, however, is that assigning a reference to null does not in any way force the garbage collector to fire up at that exact moment and remove the object from the heap. The only thing you have accomplished is explicitly clipping the connection between the reference and the object it previously pointed to. Given this point, setting references to null under C# is far less consequential than doing so in other C-based languages; however, doing so will certainly not cause any harm.

Determining If an Object Is Live
Now, back to the topic of how the garbage collector determines when an object is no longer needed. The garbage collector uses the following information to determine whether an object is live:
•Stack roots: Stack variables provided by the compiler and stack walker
•Garbage collection handles: Handles that point to managed objects that can be referenced from code or the runtime
•Static data: Static objects in application domains that can reference other objects

During a garbage collection process, the runtime will investigate objects on the managed heap to determine whether they are still reachable by the application. To do so, the runtime will build an object graph, which represents each reachable object on the heap. Object graphs are explained in some detail during the discussion of object serialization in Chapter 19. For now, just understand that object graphs are used to document all reachable objects. As well, be aware that the garbage collector will never graph the same object twice, thus avoiding the nasty circular reference count found in COM programming.
Assume the managed heap contains a set of objects named A, B, C, D, E, F, and G. During garbage collection, these objects (as well as any internal object references they may contain) are examined. After the graph has been constructed, unreachable objects (which you can assume are objects C and F) are marked as garbage. Figure 9-3 diagrams a possible object graph for the scenario just described (you can read the directional arrows using the phrase depends on or requires; for example, E depends on G and B, A depends on nothing, etc.).

Figure 9-3. Object graphs are constructed to determine which objects are reachable by application roots

After objects have been marked for termination (C and F in this case, as they are not accounted for in the object graph), they are swept from memory. At this point, the remaining space on the heap is
compacted, which in turn causes the runtime to modify the set of underlying pointers to refer to the correct memory location (this is done automatically and transparently). Last but not least, the next object pointer is readjusted to point to the next available slot. Figure 9-4 illustrates the resulting readjustment.

Figure 9-4. A clean and compacted heap

■ Note strictly speaking, the garbage collector uses two distinct heaps, one of which is specifically used to store large objects. This heap is less frequently consulted during the collection cycle, given possible
performance penalties involved with relocating large objects. in .NET Core, the large heap can be compacted on demand or when optional hard limits for absolute or percentage memory usage is reached.

Understanding Object Generations
When the runtime is attempting to locate unreachable objects, it does not literally examine every object placed on the managed heap. Doing so, obviously, 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 class that defined the main window of a desktop application will be in memory until the program terminates. Conversely, objects that have only recently been placed on the heap (such as an object allocated within a method scope) are likely to be unreachable rather quickly. Given these assumptions, each object on the heap belongs to a collection in one of the following generations:
• Generation 0: Identifies a newly allocated object that has never been marked for collection (with the exception of large objects, which are initially placed in a generation 2 collection). Most objects are reclaimed for garbage collection in generation 0 and do now survive to generation 1.
• Generation 1: Identifies an object that has survived a garbage collection. This generation also serves as a buffer between short-lived objects and long-lived objects.
• Generation 2: Identifies an object that has survived more than one sweep of the garbage collector or a significantly large object that started in a generation 2 collection.

■ Note generations 0 and 1 are termed ephemeral generations. as explained in the next section, you will see that the garbage collection process does treat ephemeral generations differently.

The garbage collector will investigate all generation 0 objects first. If marking and sweeping (or said more plainly, getting rid of) these objects results in the required amount of free memory, any surviving objects are promoted to generation 1. To see how an object’s generation affects the collection process, ponder Figure 9-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 9-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 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 evaluated. 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 main window) are not “bothered” as often.
Garbage collection is triggered when the system has low physical memory, when memory allocated on the managed heap rises above an acceptable threshold, or when GC.Collect() is called in the application code.
If this all seems a bit wonderful and better than having to manage memory yourself, remember that the process of garbage collection is not without some cost. The timing of garbage collection and what gets collected when are typically out of the developers’ controls, although garbage collection can certainly
be influenced for good or bad. And when garbage collection is executing, CPU cycles are being used and can affect the performance of the application. The next sections examine the different types of garbage collection.

Ephemeral Generations and Segments
As mentioned earlier, generations 0 and 1 are short-lived and are known as ephemeral generations. These generations are allocated in a memory segment known as the ephemeral segment. As garbage collection occurs, new segments acquired by the garbage collection become new ephemeral segments, and the segment containing objects surviving past generation 1 becomes the new generation 2 segment.
The size of the ephemeral segment varies on a number of factors, such as the garbage collection type (covered next) and the bitness of the system. Table 9-1 shows the different sizes of the ephemeral segments.

Table 9-1. Ephemeral Segment Sizes

Garbage Collection Type 32-bit 64-bit
Workstation 16 MB 256 MB
Server 64 MB 4 GB
Server with > 4 logical CPUs 32 MB 2 GB
Server with > 8 logical CPUs 16 MB 1 GB

Garbage Collection Types
There are two types of garbage collection provided by the runtime:
•Workstation garbage collection: This is designed for client applications and is the default for stand-alone applications. Workstation GC can be background (covered next) or nonconcurrent.
•Server garbage collection: This is designed for server applications that require high throughput and scalability. Server GC can be background or nonconcurrent, just like workstation GC.

■ Note The names are indicative of the default settings for workstation and server applications, but the method of garbage collection is configurable through the machine’s runtimeconfig.json or system environment variables. Unless the computer has only one processor, then it will always use workstation garbage collection.

Workstation GC occurs on the same thread that triggered the garbage collection and remains at the same priority as when it was triggered. This can cause competition with other threads in the application.
Server GC occurs on multiple dedicated threads that are set to the THREAD_PRIORITY_HIGHEST priority level (threading is covered in Chapter 15). Each CPU gets a dedicated heap and dedicated thread to perform garbage collection. This can lead to server garbage collection becoming very resource intensive.

Background Garbage Collection
Beginning with .NET 4.0 (and continuing in .NET Core), the garbage collector is able to deal with thread suspension when it cleans up objects on the managed heap, using background garbage collection. Despite its name, this does not mean that all garbage collection now takes place on additional background threads of execution. Rather, if a background garbage collection is taking place for objects living in a nonephemeral

generation, the .NET Core runtime is now able to collect objects on the ephemeral generations using a dedicated background thread.
On a related note, the .NET 4.0 and higher garbage collection have been improved to further reduce the amount of time a given thread involved with garbage collection details must be suspended. The end result of these changes is that the process of cleaning up unused objects living in generation 0 or generation 1 has been optimized and can result in better runtime performance of your programs (which is really important for real-time systems that require a small, and predictable, GC stop time).
Do understand, however, that the introduction of this new garbage collection model has no effect on how you build your .NET Core applications. For all practical purposes, you can simply allow the garbage collector to perform its work without your direct intervention (and be happy that the folks at Microsoft are improving the collection process in a transparent manner).

The System.GC Type
The mscorlib.dll assembly provides 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 aware that you will seldom (if ever) need to make use of this class directly in your code. Typically, the only time you will use the members of System.GC is when you are creating classes that make internal use of unmanaged resources. This could be the case if you are building a class that makes calls into the Windows C-based API using the .NET Core platform invocation protocol or perhaps because of some very low-level and complicated COM interop
logic. Table 9-2 provides a rundown of some of the more interesting members (consult the .NET Framework SDK documentation for complete details).

Table 9-2. Select Members of the System.GC Type

System.GC Member Description
AddMemoryPressure() RemoveMemoryPressure() Allows you to specify a numerical value that represents the calling 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. This method has been overloaded to specify a generation to collect, as well as the mode of collection (via the GCCollectionMode enumeration).
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. 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, update your top-level statements of the SimpleGC project to the following, which makes use of several members of GC:

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

After running this, you should see output similar to this:

Fun with System.GC

Estimated bytes on heap: 75760 This OS has 3 object generations.

Zippy is going 100 MPH Generation of refToMyCar is: 0

You will explore more of the methods from Table 9-2 in the next section.

Forcing a Garbage Collection
Again, the whole purpose of the garbage collector is to manage memory on your behalf. However, in some 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:
• Your application is about to enter 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:


// 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 runtime to investigate only generation 0 objects, you would write the following:


// Only investigate generation 0 objects.

GC.Collect(0); GC.WaitForPendingFinalizers();

As well, the Collect() method can 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 your top-level statements have been updated as follows:

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. Console.WriteLine("Force Garbage Collection"); 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, I have purposely created a large array of object types (50,000 to be exact) for testing purposes.
Here is the output from the program:

Fun with System.GC

Estimated bytes on heap: 75760 This OS has 3 object generations.

Zippy is going 100 MPH Generation of refToMyCar is: 0 Forcing Garbage Collection 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, you’ll examine the garbage collection process a bit further by addressing how you can build finalizable objects, as well as disposable objects. Be aware that the following techniques are typically necessary only if you are building C# classes that maintain internal unmanaged resources.

Building Finalizable Objects
In Chapter 6, you learned that the supreme base class of .NET Core, 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.

■ 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). Remember from Chapter 4 that ref structs and read-only ref structs can’t implement an interface but can implement a Dispose() method.

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 prior versions of .NET (not .NET Core), each object’s finalizer is called on application shutdown. In .NET Core, there isn’t any way to force the finalizer to be executed, even when the app is shut down.
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 Core 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 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 using 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 runtime 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 project 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.

namespace SimpleFinalize; class MyResourceWrapper

{
// Compile-time error!
protected override void Finalize(){ }
}

Rather, when you want to configure your custom C# class types to override the Finalize() method, you make use of (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 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).
The 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
{
// Clean up unmanaged resources here.
// Beep when destroyed (testing purposes only!)
~MyResourceWrapper() => 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
{
.override [System.Runtime]System.Object::Finalize
// Code size 17 (0x11)
.maxstack 1
.try
{
IL_0000: call void [System.Console]System.Console::Beep() IL_0005: nop
IL_0006: leave.s IL_0010
} // end .try finally
{
IL_0008: ldarg.0
IL_0009: call instance void [System.Runtime]System.Object::Finalize() IL_000e: nop
IL_000f: endfinally

} // end handler IL_0010: ret
} // end of method MyResourceWrapper::Finalize

If you then tested the MyResourceWrapper type, you would find that a system beep occurs when the finalizer executes.

using SimpleFinalize;

Console.WriteLine(" Fun with Finalizers \n"); Console.WriteLine("Hit return to create the objects "); Console.WriteLine("then force the GC to invoke Finalize()");
//Depending on the power of your system,
//you might need to increase these values CreateObjects(1_000_000);
//Artificially inflate the memory pressure GC.AddMemoryPressure(2147483647); GC.Collect(0, GCCollectionMode.Forced); GC.WaitForPendingFinalizers(); Console.ReadLine();

static void CreateObjects(int count)
{
MyResourceWrapper[] tonsOfObjects = new MyResourceWrapper[count];
for (int i = 0; i < count; i++)
{
tonsOfObjects[i] = new MyResourceWrapper();
}
tonsOfObjects = null;
}

■ Note The only way to guarantee that this small console app will force a garbage collection in .NET Core is to create a huge amount of the objects in memory and then set them to null. if you run this sample app, make sure to hit the Ctrl+C key combination to stop the program execution and all of the beeping!

Detailing the Finalization Process
It’s important to always remember that the role of the Finalize() method is to ensure that a .NET Core 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 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 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 the 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, because of the extra behind-the-curtains processing, considerably slower.

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 Non-ref structures and class types both can implement IDisposable (unlike overriding Finalize(), which is reserved for class types), as the object user (not the garbage collector) invokes the Dispose() method. disposable ref structs were covered in Chapter 4.

To illustrate the use of this interface, create a new C# Console Application project named SimpleDispose. Here is an updated MyResourceWrapper class that now implements IDisposable, rather than overriding System.Object.Finalize():

namespace SimpleDispose;
// 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 not only is 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 on the managed heap and has access to all other heap-allocated objects. The calling logic, shown here, is straightforward:

using SimpleDispose;
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 documentation, a programmatic check can be accomplished using the is or as keyword discussed in Chapter 6.

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:

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:

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

Console.WriteLine(" Fun with Dispose \n");
// Dispose() is called automatically when the using scope exits. using(MyResourceWrapper rw = new MyResourceWrapper())
{
// Use rw object.
}

If you looked at the following CIL code of the top-level statements 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 ‘

$'(string[] args) cil managed
{

.try
{
} // end .try finally
{
IL_0019: callvirt instance void [System.Runtime]System.IDisposable::Dispose()
} // end handler
} // end of method ‘$’::’
$’

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

// Use a comma-delimited list to declare multiple objects to dispose. using(MyResourceWrapper rw = new MyResourceWrapper(), rw2 = new MyResourceWrapper())
{
// Use rw and rw2 objects.
}

Using Declarations (New 8.0)
New in C# 8.0 is the addition of using declarations. A using declaration is a variable declaration preceded by the using keyword. This is functionally the same as the syntax covered in the last question, with the exception of the explicit code block marked by braces ({}).
Add the following method to your class:

private static void UsingDeclaration()
{
//This variable will be in scope until the end of the method using var rw = new MyResourceWrapper();
//Do something here Console.WriteLine("About to dispose.");
//Variable is disposed at this point.
}

Next, add the following call to the top-level statements:

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


Console.WriteLine("Demonstrate using declarations"); UsingDeclaration();
Console.ReadLine();

If you examine the new method with ILDASM, you will (as you might expect) find the same code as before.

.method private hidebysig static
void UsingDeclaration() cil managed
{

.try
{

} // end .try finally
{

IL_0018: callvirt instance void [System.Runtime]System.IDisposable::Dispose()

} // end handler IL_001f: ret
} // end of method Program::UsingDeclaration

This new feature is essentially compiler magic, saving a few keystrokes. Be careful when using it, as the new syntax is not as explicit as the previous syntax.

Building Finalizable and Disposable Types
At this point, you have seen two different approaches to constructing a class that cleans up internal unmanaged resources. On the one hand, you can use a finalizer. Using this technique, you have the peace of mind that comes with knowing the object cleans itself up when garbage collected (whenever that may be) without the need for user interaction. On the other hand, you can implement IDisposable to provide a way for the object user to clean up the object as soon as it is finished. However, if the caller forgets to call Dispose(), the unmanaged resources may be held in memory indefinitely.
As you might suspect, it is possible to blend both techniques into a single class definition. By doing so, you gain the best of both models. If the object user does remember to call Dispose(), you can inform the garbage collector to bypass the finalization process by calling GC.SuppressFinalize(). If the object user forgets to call Dispose(), the object will eventually be finalized and have a chance to free up the internal resources. The good news is that the object’s internal unmanaged resources will be freed one way or another.
Here is the next iteration of MyResourceWrapper, which is now finalizable and disposable, defined in a C# Console Application project named FinalizableDisposableClass:

namespace FinalizableDisposableClass;
// A sophisticated resource wrapper.
public class MyResourceWrapper : IDisposable
{
// The garbage collector will call this method if the object user forgets to call Dispose().

~MyResourceWrapper()
{
// Clean up any internal unmanaged resources.
// Do not call Dispose() on any managed objects.
}
// The object user will call this method to clean up resources ASAP. public void Dispose()
{
// Clean up unmanaged resources here.
// Call Dispose() on other contained disposable objects.
// No need to finalize if user called Dispose(), so suppress finalization. GC.SuppressFinalize(this);
}
}

Notice that this Dispose() method has been updated to call GC.SuppressFinalize(), which informs the runtime that it is no longer necessary to call the destructor when this object is garbage collected, given that the unmanaged resources have already been freed via the Dispose() logic.

A Formalized Disposal Pattern
The current implementation of MyResourceWrapper does work fairly well; however, you are left with a few minor drawbacks. First, the Finalize() and Dispose() methods each have to clean up the same unmanaged resources. This could result in duplicate code, which can easily become a nightmare to maintain. Ideally, you would define a private helper function that is called by either method.
Next, you’d like to make sure that the Finalize() method does not attempt to dispose of any managed objects, while the Dispose() method should do so. Finally, you’d also like to be certain the object user can safely call Dispose() multiple times without error. Currently, the Dispose() method has no such safeguards.
To address these design issues, Microsoft defined a formal, prim-and-proper disposal pattern that strikes a balance between robustness, maintainability, and performance. Here is the final (and annotated) version of MyResourceWrapper, which makes use of this official pattern:

class MyResourceWrapper : IDisposable
{
// Used to determine if Dispose() has already been called. private bool disposed = false;

public void Dispose()
{
// Call our helper method.
// Specifying "true" signifies that the object user triggered the cleanup. CleanUp(true);

// Now suppress finalization. GC.SuppressFinalize(this);
}

private void CleanUp(bool disposing)
{
// Be sure we have not already been disposed! if (!this.disposed)

{

// If disposing equals true, dispose all managed resources. if (disposing)
{
// Dispose managed resources.
}
// Clean up unmanaged resources here.
}
disposed = true;
}
~MyResourceWrapper()
{
// Call our helper method.
// Specifying "false" signifies that the GC triggered the cleanup. CleanUp(false);
}
}

Notice that MyResourceWrapper now defines a private helper method named CleanUp(). By specifying true as an argument, you indicate that the object user has initiated the cleanup, so you should clean up all managed and unmanaged resources. However, when the garbage collector initiates the cleanup, you specify false when calling CleanUp() to ensure that internal disposable objects are not disposed (as you can’t assume they are still in memory!). Last but not least, the bool member variable (disposed) is set to true before exiting CleanUp() to ensure that Dispose() can be called numerous times without error.

■ Note after an object has been “disposed,” it’s still possible for the client to invoke members on it, as it is still in memory. Therefore, a robust resource wrapper class would also need to update each member of the class with additional coding logic that says, in effect, “if i am disposed, do nothing and return from the member.”

To test the final iteration of MyResourceWrapper, update your Program.cs file to the following:

using FinalizableDisposableClass;

Console.WriteLine(" Dispose() / Destructor Combo Platter ");

// Call Dispose() manually. This will not call the finalizer. MyResourceWrapper rw = new MyResourceWrapper();
rw.Dispose();

// Don’t call Dispose(). This will trigger the finalizer when the object gets GCd. MyResourceWrapper rw2 = new MyResourceWrapper();

Notice that you are explicitly calling Dispose() on the rw object, so the destructor call is suppressed.
However, you have “forgotten” to call Dispose() on the rw2 object; no worries—the finalizer will still execute when the object is garbage collected.
That concludes your investigation of how the runtime manages your objects via garbage collection. While there are additional (somewhat esoteric) details regarding the collection process I haven’t covered here (such as weak references and object resurrection), you are now in a perfect position for further

exploration on your own. To wrap up this chapter, you will examine a programming feature called lazy instantiation of objects.

Understanding Lazy Object Instantiation
When you are creating classes, you might occasionally need to account for a particular member variable in code, which might never actually be needed, in that the object user might not call the method (or property) that makes use of it. Fair enough. However, this can be problematic if the member variable in question requires a large amount of memory to be instantiated.
For example, assume you are writing a class that encapsulates the operations of a digital music player. In addition to the expected methods, such as Play(), Pause(), and Stop(), you also want to provide the ability to return a collection of Song objects (via a class named AllTracks), which represents every single digital music file on the device.
If you’d like to follow along, create a new Console Application project named LazyObjectInstantiation, and define the following class types:

//Song.cs
namespace LazyObjectInstantiation;
// Represents a single song.
class Song
{
public string Artist { get; set; } public string TrackName { get; set; } public double TrackLength { get; set; }
}

//AllTracks.cs
namespace LazyObjectInstantiation;
// Represents all songs on a player.
class AllTracks
{
// Our media player can have a maximum
// of 10,000 songs.
private Song[] _allSongs = new Song[10000];

public AllTracks()
{
// Assume we fill up the array
// of Song objects here. Console.WriteLine("Filling up the songs!");
}
}

//MediaPlayer.cs
namespace LazyObjectInstantiation;
// The MediaPlayer has-an AllTracks object.
class MediaPlayer
{
// Assume these methods do something useful. public void Play() { / Play a song / }

public void Pause() { / Pause the song / } public void Stop() { / Stop playback / } private AllTracks _allSongs = new AllTracks();

public AllTracks GetAllTracks()
{
// Return all of the songs. return _allSongs;
}
}

The current implementation of MediaPlayer assumes that the object user will want to obtain a list of songs via the GetAllTracks() method. Well, what if the object user does not need to obtain this list? In the current implementation, the AllTracks member variable will still be allocated, thereby creating 10,000 Song objects in memory, as follows:

using LazyObjectInstantiation;

Console.WriteLine(" Fun with Lazy Instantiation \n");

// This caller does not care about getting all songs,
// but indirectly created 10,000 objects! MediaPlayer myPlayer = new MediaPlayer(); myPlayer.Play();

Console.ReadLine();

Clearly, you would rather not create 10,000 objects that nobody will use, as that will add a good deal of stress to the .NET Core garbage collector. While you could manually add some code to ensure the _allSongs object is created only if used (perhaps using the factory method design pattern), there is an easier way.
The base class libraries provide a useful generic class named Lazy<>, defined in the System namespace of mscorlib.dll. This class allows you to define data that will not be created unless your code base actually uses it. As this is a generic class, you must specify the type of item to be created on first use, which can be any type with the .NET Core base class libraries or a custom type you have authored yourself. To enable lazy instantiation of the AllTracks member variable, you can simply update the MediaPlayer code to this:

// The MediaPlayer has-an Lazy object. class MediaPlayer
{

private Lazy _allSongs = new Lazy(); public AllTracks GetAllTracks()
{
// Return all of the songs. return _allSongs.Value;
}
}

Beyond the fact that you are now representing the AllTracks member variable as a Lazy<> type, notice that the implementation of the previous GetAllTracks() method has also been updated. Specifically, you

must use the read-only Value property of the Lazy<> class to obtain the actual stored data (in this case, the
AllTracks object that is maintaining the 10,000 Song objects).
With this simple update, notice how the following updated code will indirectly allocate the Song objects only if GetAllTracks() is indeed called:

Console.WriteLine(" Fun with Lazy Instantiation \n");

// No allocation of AllTracks object here! MediaPlayer myPlayer = new MediaPlayer(); myPlayer.Play();

// Allocation of AllTracks happens when you call GetAllTracks().
MediaPlayer yourPlayer = new MediaPlayer(); AllTracks yourMusic = yourPlayer.GetAllTracks();

Console.ReadLine();

■ Note Lazy object instantiation is useful not only to decrease allocation of unnecessary objects. You can also use this technique if a given member has expensive creation code, such as invoking a remote method, communicating with a relational database, etc.

Customizing the Creation of the Lazy Data
When you declare a Lazy<> variable, the actual internal data type is created using the default constructor, like so:

// Default constructor of AllTracks is called when the Lazy<>
// variable is used.
private Lazy _allSongs = new Lazy();

While this might be fine in some cases, what if the AllTracks class had some additional constructors and you want to ensure the correct one is called? Furthermore, what if you have some extra work to do (beyond simply creating the AllTracks object) when the Lazy<> variable is made? As luck would have it, the Lazy<> class allows you to specify a generic delegate as an optional parameter, which will specify a method to call during the creation of the wrapped type.
The generic delegate in question is of type System.Func<>, which can point to a method that returns the same data type being created by the related Lazy<> variable and can take up to 16 arguments (which are typed using generic type parameters). In most cases, you will not need to specify any parameters
to pass to the method pointed to by Func<>. Furthermore, to greatly simplify the use of the required Func<>, I recommend using a lambda expression (see Chapter 12 to learn or review the delegate/lambda relationship).
With this in mind, the following is a final version of MediaPlayer that adds a bit of custom code when the wrapped AllTracks object is created. Remember, this method must return a new instance of the type wrapped by Lazy<> before exiting, and you can use any constructor you choose (here, you are still invoking the default constructor of AllTracks).

class MediaPlayer

{

// Use a lambda expression to add additional code
// when the AllTracks object is made. private Lazy _allSongs =
new Lazy( () =>
{
Console.WriteLine("Creating AllTracks object!"); return new AllTracks();
}
);

public AllTracks GetAllTracks()
{
// Return all of the songs. return _allSongs.Value;
}
}

Sweet! I hope you can see the usefulness of the Lazy<> class. Essentially, this generic class allows you to ensure expensive objects are allocated only when the object user requires them.

Summary
The point of this chapter was to demystify the garbage collection process. As you saw, the garbage collector will run only when it is unable to acquire the necessary memory from the managed heap (or when the developer calls GC.Collect()). When a collection does occur, you can rest assured that Microsoft’s collection algorithm has been optimized by the use of object generations, secondary threads for the purpose of object finalization, and a managed heap dedicated to hosting large objects.
This chapter also illustrated how to programmatically interact with the garbage collector using the System.GC class type. As mentioned, the only time you will really need to do so is when you are building finalizable or disposable class types that operate on unmanaged resources.
Recall that finalizable types are classes that have provided a destructor (effectively overriding the Finalize() method) to clean up unmanaged resources at the time of garbage collection. Disposable objects, on the other hand, are classes (or non-ref structures) that implement the IDisposable interface, which should be called by the object user when it is finished using said objects. Finally, you learned about an official “disposal” pattern that blends both approaches.
This chapter wrapped up with a look at a generic class named Lazy<>. As you saw, you can use this class to delay the creation of an expensive (in terms of memory consumption) object until the caller actually requires it. By doing so, you can help reduce the number of objects stored on the managed heap and also ensure expensive objects are created only when actually required by the caller.

发表评论