mr-ponna.com

 

YOU ARE HERE: HOME Articles Understanding Object Lifetime



Understanding Object Lifetime

View(s): 5597

Understanding Object Lifetime

.NET objects are allocated in a memory called managed heap, where they will be automatically destroyed by the garbage collector.

Before going into details you have to know about the class, object, reference, stack and heap.

A class is nothing but 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).

Consider a simple Car class defined in a C# Console Application project named SimpleGC:

    //Car.cs
    public class Car
    {
        private int currSp;
        private string petName;
        public Car() { }
        public Car(string name, int speed)
        {
            petName = name;
            currSp = speed;
        }

        public override string ToString()
        {
            return string.Format("{0} is going {1} MPH",
            petName, currSp);
        }
    }

Once a class is defined, you can allocate any number of objects using the C# new keyword.

Understand, that the new keyword returns a reference to the object on the heap, not the

Actual object itself. This reference variable is stored on the stack for further use in your application.

When you wish to call members on the object, apply the C# dot operator to the stored reference:

    class Program
    {
        static void Main(string[] args)
        {
            // Create a new Car object on
            // the managed heap. We are
            // returned a reference to this
            // object ("refToMyCar").
            Car refToMyCar = new Car("Benz", 50);
            // The C# dot operator (.) is used
            // to call members on the object
            // using our reference variable.
            Console.WriteLine(refToMyCar.ToString());
            Console.ReadLine();
        }
    }

Below Figure shows the class, object, and reference relationship.

  References to objects on the managed heap

The Basics of Object Lifetime

When you are building your C# applications, you are correct to assume that the managed heap will

take care of itself without your direct interference. In fact, the golden rule of .NET memory management

is simple:

Rule:  Allocate an object onto the managed heap using the new keyword and forget about it.

Once instantiated, the garbage collector will destroy the object when it is no longer needed.

The next obvious question, for reader is:

“How does the garbage collector decide when the object is no longer needed in Managed heap?”

The short answer:
Garbage collector removes an object from the heap when it is no longer used by your code base.

Assume you have a method in your Program class that allocates a local Car object:

  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 the 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 cannot

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 CLR performs the next garbage collection, the myCar object could be safely destroyed.

The CIL of new

When the C# compiler encounters the new keyword, it will emit a CIL newobj instruction into the

Method implementation. If you were to compile the current example code and investigate the resulting assembly using ildasm.exe, you would find the following CIL statements within the

MakeACar() method:

Before we 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 CLR. The .NET garbage collector is quite a tidy housekeeper of the heap, given that it will compact empty blocks of memory (when necessary) for 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.

These things being said, the newobj instruction informs the CLR to perform the following core tasks:

• Calculate the total amount of memory required for the object to be allocated (including the necessary memory required by the type’s data members and the type’s base classes).

• Examine the managed heap to ensure that there is indeed enough room to host the object to be allocated. If this is the case, the type’s 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.

• Finally, before returning the reference to the caller, advance the next object pointer to point to the next available slot on the managed heap.

Below Figure explains 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 CLR 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.

When a collection does take place, the garbage collector temporarily suspends all active threads within the current process to ensure that the application does not access the heap during the collection process. (A thread as a path of execution within a running executable). Once the garbage collection cycle has completed, the suspended threads are allowed to carry on their work. Thankfully, the .NET garbage collector is highly optimized.

Setting Object References to null

Given these facts, 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 will generate CIL code that ensures the reference (myCar in this example) no longer points to any object. If you were once again to make 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 allocated Car):

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. However, doing so will certainly not cause any harm.

The Role of Application Roots

Now, back to the topic of how the garbage collector determines when an object is “no longer needed.” To understand the details, you need to be aware of the notion of application roots. Simply put, a root is a storage location containing a reference to an object on the heap. Strictly speaking, a root can fall into any of the following categories:

• References to global objects (while not allowed in C#, CIL code does permit allocation of global objects)
• References to any static objects/static fields
• References to local objects within an application’s code base
• References to object parameters passed into a method
• References to objects waiting to be finalized (described later in this chapter)
• Any CPU register that references an object

During a garbage collection process, the runtime will investigate objects on the managed heap to determine whether they are still reachable (rooted) by the application. To do so, the CLR will build an object graph, which represents each reachable object on the heap. 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

Assume the managed heap contains a set of objects named A, B, C, D, E, F, and G. During a garbage collection, these objects (as well as any internal object references they may contain) are examined for active roots. Once the graph has been constructed, unreachable objects (which we will assume are objects C and F) are marked as garbage.

Below 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 indirectly B,” “A depends on nothing,” and so on).

Object graphs are constructed to determine which objects are reachable by application roots.

Once an object has been marked for termination (C and F in this case—as they are not accounted for in the object graph), they are cleaned from memory. At this point, the remaining space on the heap is compacted, which in turn will cause the CLR to modify the set of active application roots (and the 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.

Below Figure illustrates clean and compacted heap.

Understanding Object Generations

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

To help optimize the process, each object on the heap is assigned to a specific “generation.” The idea behind generations is simple: the longer an object has existed on the heap, the more likely it is to stay there. For example, the object implementing Main() will be in memory until the program terminates. Conversely, objects that have been recently placed on the heap (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 one of the following generations:

• Generation 0: Identifies a newly allocated object that has never been marked for collection
• Generation 1: Identifies an object that has survived a garbage collection (i.e., it was marked for collection, but was not removed due to the fact that the sufficient heap space was acquired)
• Generation 2: Identifies an object that has survived more than one sweep of the garbage collector

The garbage collector will investigate all generation 0 objects first. If marking and sweeping these objects results in the required amount of free memory, any surviving objects are promoted to generation 1. To illustrate how an object’s generation affects the collection process, ponder

Below Figure which diagrams how a set of surviving generation 0 objects (A, B, and E) are promoted once the required memory has been reclaimed.

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

If all generation 0 objects have been evaluated, but additional memory is still required, generation

1 objects are then investigated for their “reachability” and collected accordingly. Surviving

generation 1 objects are then promoted to generation 2. If the garbage collector still requires additional memory, generation 2 objects are then evaluated for their reachability. At this point, if a generation 2 object survives a garbage collection, it remains a generation 2 object given the predefined upper limit of object generations.

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

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, under some very rare circumstances, it may be beneficial to programmatically force a garbage collection using GC.Collect(). Specifically:

• Your application is about to enter into a block of code that you do not wish to be interrupted by a possible garbage collection.
• Your application has just finished allocating an extremely large number of objects and you wish to remove as much of the acquired memory as possible.

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

static void Main(string[] args)
{
// Force a garbage collection and wait for
// each object to be finalized.
    GC.Collect();
    GC.WaitForPendingFinalizers();
}

When you manually force a garbage collection, you should always make a call to GC.WaitForPendingFinalizers().With this approach, you can rest assured that all finalizable objects have had a chance to perform any necessary cleanup before your program continues forward. Under the hood, GC.WaitForPendingFinalizers() will suspend the calling “thread” during the collection process. This is a good thing, as it ensures your code does not invoke methods on an object currently being destroyed!

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

static void Main(string[] args)
{
// Only investigate generation 0 objects.
GC.Collect(0);
GC.WaitForPendingFinalizers();
}

As well, as of .NET 3.5, 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.
}

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

static void Main(string[] args)
{
    Console.WriteLine("***** Fun with System.GC *****");
    // 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 shown in Figure 8-6, even though this Main() method only made one explicit request for a garbage collection (via the GC.Collect() method), the CLR performed a number of them in the background.

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

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


  Last updated on Tuesday, 03 December 2013
  Author: Kishore Kumar
4/5 stars (9 vote(s))

Register Login Ask Us Write to Us Help