Memory Organization and Garbage Collection - Diving Deeper - Sams Teach Yourself C# 5.0 in 24 Hours (2013)

Sams Teach Yourself C# 5.0 in 24 Hours (2013)

Part V: Diving Deeper

Hour 23. Memory Organization and Garbage Collection


What You’ll Learn in This Hour

Memory organization

Garbage collection

Understanding the IDisposable interface

Using the dispose pattern

Declaring and using finalizers


In Hour 1, “The .NET Framework and C#,” you learned that one of the benefits provided by the .NET Framework is automatic memory management. This helps you create applications that are more stable by preventing many common programming errors and enables you to focus your time on the business logic your application requires. Even with automatic memory management, it is still important to understand how the garbage collector interacts with your program and the types you create.

You briefly learned about value and reference types in Hour 3, “Understanding C# Types.” The simple definition for a value type presented was that a value type is completely self-contained and copied “by value.” A reference type, on the other hand, contains a reference to the actual data. Because variables of a value type directly contain their data, it is not possible for operations on one to affect the other. It is possible for two variables of a reference type to refer to the same object, allowing operations on one variable to affect the other.

In this hour, you learn some of the fundamentals of how memory is organized in the common language runtime (CLR), how the garbage collector works, and how the .NET Framework provides mechanisms for deterministic finalization.

Memory Organization

To better understand how types work, you need to have a basic understanding of the two mechanisms the CLR uses to store data in memory: the stack and the heap. Figure 23.1 shows a conceptual view of stack and heap memory.

Image

Figure 23.1. Stack and heap memory.

The simplest way to think of stack memory is that it is organized like a stack of plates in a restaurant. The last plate placed on the stack is the first one removed. This is also known as a last-in first-out (LIFO) queue. Stack memory is used by the .NET Framework to store local variables (except for the local variables used in iterator blocks or those captured by a lambda or anonymous method) and temporary values. You can think of stack memory as providing cheap garbage collection because the lifetime of variables placed on the stack is well known.

Heap memory, however, is more like a wall of empty slots. Each slot can indicate if it is already full or if it is no longer used and ready to be recycled. When a slot has been filled, its contents can be replaced only with something that is the same type as it originally contained. A slot can be reused (and have its type changed) only when it has been recycled.

Garbage Collection

A type defined by a class is a reference type. When you create an instance of a reference type using the new operator, the runtime actually performs several actions on your behalf. The two primary actions that occur follow:

1. Memory is allocated and initialized to its default value.


Go To

TABLE 3.12, “DEFAULT VALUES,” to determine the value used.


2. The constructor for the class is executed to initialize the allocated memory.

At this point, your object is initialized and ready to be used. Because the runtime allocated the memory on your behalf, it is reasonable to expect that it also deallocates that memory at some undetermined future point in time. The responsibility for deallocating memory falls to the garbage collector.

The simple view of how the garbage collector works is that when it runs, the following actions occur:

1. Every instance of a reference type is assumed to be “garbage.”

2. The garbage collector determines which instances are still accessible. These types are considered “live” and are marked to indicate they are still reachable.

3. The memory used by the unmarked reference types is deallocated.

4. The managed heap is then compacted (by moving memory) to reduce fragmentation and consolidate the used memory space. As the “live” objects are moved, the mark is cleared in anticipation of the next garbage collection cycle.


Note: Object Lifetime

Every object instance has a lifetime, which is the length of execution time an object is pointed to by a valid reference. As long as an object has at least one valid reference, it cannot be destroyed. By creating more references to an object, you can potentially extend its lifetime.


Understanding the IDisposable Interface

Because memory deallocation occurs at an unspecified point in time, the .NET Framework provides a mechanism, through the IDisposable interface, that enables you to provide explicit cleanup behavior before the memory is reclaimed. This interface provides a way to perform deterministic resource deallocation. It also provides a consistent pattern that types needing to control resource allocation can utilize.

Listing 23.1 shows the definition of the IDisposable interface, which provides a single public method named Dispose.

Listing 23.1. The IDisposable Interface


public interface IDisposable
{
void Dispose();
}


Types that implement this interface, commonly called disposable types, give the code using that type a way to indicate that the type is eligible for garbage collection. If the type has any unmanaged resources it maintains, calling Dispose should immediately release those unmanaged resources; however, calling the Dispose method does not actually cause the instance to be garbage collected.

The using Statement

Even if a type implements IDisposable, there is no way for the .NET Framework to ensure that you have actually called the Dispose method. This problem is compounded when you consider the implications of an exception occurring. If an exception occurs, there is a possibility that, depending on how the calling code is written, the disposable type will never get the opportunity to actually perform its cleanup.

From Hour 11, “Handling Errors Using Exceptions,” you learned that you could place the calling code in a try-finally block where the call to the Dispose method is placed in the finally handler. Although this is the correct implementation, it can be easy to get incorrect (particularly when you need to nest multiple protected regions) and can lead to code that is hard to read.

To alleviate these problems, C# provides the using statement. You saw examples of the using statement in Hour 14, “Using Files and Streams.” The using statement provides a clean and simple way to indicate the intended lifetime of an object and ensures the Dispose method is called when that lifetime ends.

The syntax for the using statement is as follows:

using (resource-acquisition) embedded-statement

When the compiler encounters a using statement, it actually generates code similar to what is shown in Listing 23.2.

Listing 23.2. Compiler Generated Expansion for the using Statement


{
DisposableObject x = new DisposableObject();

try
{
// use the object.
}
finally
{
if (x != null)
{
((IDisposable)x).Dispose();
}
}
}


There are a few subtle, but important, things to note in this expansion. The first is the outermost enclosing braces. This defines a scope that contains the expansion but, more specifically, helps ensure that the variable defined in the using statement is accessible only from within that defined scope. The second is that the local variable declared for the resource acquisition is read-only and that it is a compile-time error to modify this variable from within the using block statement. Finally, because the compiler explicitly casts the object to the IDisposable interface (to ensure that it is calling the correct Dispose method), the using statement can be used only with types that implement the IDisposable interface.

Using the Dispose Pattern

To implement the dispose pattern, you provide an implementation of the IDisposable interface. However, implementing the IDisposable interface is actually only part of the pattern. The complete dispose pattern, in the context of the Contact class, is shown in Listing 23.3.


Tip: When Should You Implement the Dispose Pattern?

Typically, you should only implement the dispose pattern if

• You control unmanaged resources directly.

• You control other disposable resources directly.


Listing 23.3. Basic Dispose Pattern


public class Contact : IDisposable
{
private bool disposed;

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

protected virtual void Dispose(bool disposing)
{
if (!this.disposed)
{
if (disposing)
{
// Release only managed resources here.
}

// Release only unmanaged resource here.
this.disposed = true;
}
}
}


The public Dispose method should always be implemented as shown. The order of the two calls is important and shouldn’t be changed. This order ensures that GC.SuppressFinalize is called only if the Dispose operation completes successfully. When Dispose calls Dispose(true), the call might fail, but later on the garbage collector can call Dispose(false). In reality, these are two different calls that can execute different portions of the code, so even though Dispose(true) fails, Dispose(false) might not.

All your resource cleanup should be contained in the Dispose(bool disposing) method. If necessary, you should protect the cleanup by testing the disposing parameter. This should happen for both managed and unmanaged resources. The Dispose(bool disposing) runs in two distinct scenarios:

• If disposing is true, the method has been called directly or indirectly by a user’s code. Managed and unmanaged resources can be disposed.

• If disposing is false, the method has been called by the runtime from inside the finalizer, and you should not reference other objects. Only unmanaged resources can be disposed.

The benefit of using the dispose pattern is that it provides a way for users of the type to explicitly indicate that they are done using that instance and that its resources should be released.

Declaring and Using Finalizers

The dispose pattern is not the only way the .NET Framework enables you to perform explicit resource cleanup before the object’s memory is reclaimed. The other way is using a finalizer, which is a special method called automatically after an object becomes inaccessible. It is important to realize that the finalizer method will not be called until the garbage collector realizes the object is unreachable.

A finalizer method looks like the default constructor for a class except the method name is prefixed with the tilde (~) character. Listing 23.4 shows how you would implement a finalizer for the Contact class.

Listing 23.4. Implementing a Finalizer


public class Contact : IDisposable
{
private bool disposed;

public Contact()
{
}

~Contact()
{
Dispose(false);
}

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

protected virtual void Dispose(bool disposing)
{
if (!this.disposed)
{
if (disposing)
{
// Release only managed resources here.
}

// Release only unmanaged resource here.
this.disposed = true;
}
}
}


There are several important rules about declaring and using finalizers:

• The exact time and order of when a finalizer executes is undefined.

• The finalizer method runs on the GC thread, not the application’s main thread.

• Finalizers apply only to reference types.

• You cannot specify an access modifier for a finalizer method.

• You cannot provide parameters to a finalizer method.

• You cannot overload or override a finalizer method.

• You cannot call a finalizer method directly in your own code; only the garbage collector can call a finalizer method.

Although finalizers are typically used when you want to provide some assurance that resources will be released even if the calling code does not call Dispose, it is not necessary to provide a finalizer just because you implement the dispose pattern. However, if you do implement a finalizer, be sure to also implement the dispose pattern.


Caution: Finalizers

Finalizers are actually difficult to write correctly because many of the normal assumptions you can make about the state of the runtime environment are not true during object finalization.

Making your class finalizable means that it cannot be garbage collected until after the finalizer has run, which means your class survives at least one extra garbage collection cycle. In addition, if not written carefully, finalizer methods have the possibility of creating a new reference to the instance being finalized, in which case the instance is alive again.


Summary

The .NET Framework does an excellent job at handling memory allocation and deallocation on your behalf, enabling you to focus on the business logic required by your application. It is, however, beneficial to have at least a basic understanding of how the .NET Framework manages memory and, more important, what mechanisms it provides to enable you to influence that management.

In this hour, you learned about the different ways the .NET Framework manages memory, through the managed heap and the stack. You saw how the using statement enables you to ensure a disposable object has its Dispose method called and how to implement the IDisposable interface in your own classes through the dispose pattern.

Q&A

Q. What are the two mechanisms the CLR uses to store data in memory?

A. The CLR uses stack and heap memory to store data.

Q. What is object lifetime?

A. Every object instance has a lifetime, which is the length of execution time an object is pointed to by a valid reference.

Q. What is one purpose of the IDisposable interface?

A. The IDisposable interface is intended to provide a way to perform deterministic resource deallocation. It also provides a consistent pattern that types which need to control resource allocation can utilize.

Q. Can the using statement be used with types that do not implement IDisposable?

A. No, the IDisposable interface is required because the compiler-generated expansion of the using statement casts the object to the interface to ensure that the correct implementation of the Dispose method is called.

Q. When should you implement the dispose pattern?

A. Typically you should only implement the dispose pattern if

• You control unmanaged resources directly.

• You control other disposable resources directly.

Q. If you implement IDisposable, should you also implement a finalizer?

A. No, just because you implement the IDisposable interface does not mean that you should also implement a finalizer. However, if you do implement a finalizer, you should also implement the IDisposable interface.

Workshop

Quiz

1. What are the two primary actions that occur on your behalf when using the new operator?

2. Does calling Dispose immediately cause the disposable object to be garbage collected?

3. What is the implication on garbage collection of implementing a finalizer?

Answers

1. The two primary actions that occur are

a. Memory is allocated.

b. The constructor for the class is executed to initialize the allocated memory.

2. No, calling Dispose does not cause the object to be immediately garbage collected. It can, however, cause the object to immediately release any unmanaged resources it maintains.

3. Making your class finalizable means that it cannot be garbage collected until after the finalizer has run, which means your class survives at least one extra garbage collection cycle. In addition, if not written carefully, finalizer methods have the possibility of creating a new reference to the instance being finalized, in which case the instance is alive again.

Exercises

There are no exercises for this hour.