A short note on object life cycle in the .NET Framework (C#)

Dinesh Jethoe
9 min readApr 30, 2020

--

The life cycle of an object is simply the time between when an object is created in memory and when it is destroyed from it.

Types in the .NET Framework

The C# programming language is a statically typed language.

Statically (strongly) vs dynamically (weakly) typed

  • Statically typed means that type checking happens at the compile time. Every variable and every expression has a type that is known at compile time. And that helps detect errors at compile time. Conversions between unrelated types are done explicitly.
  • Dynamically typed means that type checking happens at the run-time. Conversions between unrelated types are done implicitly.

The C# typing system contains three different categories:

  1. Value types: a type that is defined by either a struct or an enum. It holds data in its own memory allocation.
  2. Reference types: a type that is defined by either class, interface, or delegate. It holds a pointer to a memory location that contains the data.
  3. Pointer types: used for unsafe code and pointer arithmetic.

Memory types

There are two memory areas where data can be stored by the Common Language Runtime (CLR = the runtime environment that executes C# programs) during the execution of a .NET application namely the (managed) Heap and the Stack.

The Heap is used to store instances of reference type. The reference (memory address/pointer/variable) is stored in a stack while the object (the value) is allocated in the heap.

Instances of value type can also be stored on the Heap when:

  • value type is part of a class: the value of a value type is part of an object (reference type).
  • value type is boxed: converting a value type (e.g. char, int, float) to a reference type (object). The opposite process is called unboxing.
  • value type is an array: e.g. int[], char[] etc.
  • value type is a static variable: any static field in a type.
  • value type is used in an async or iterator block; e.g. an async method that returns Task<int>.
  • value type is closed-over locals of a lambda or anonymous method: when using anonymous method the compiler generates a new, hidden class that encapsulates the non-local variables as fields of the class and the code you include in the anonymous method or lambda expression. These non-local variables are stored on the Heap since it’s a part of the object now (read about closures in C#).

The Stack is the memory location where short-lived temporary instances of value types (local or temporary variables) can be stored but is also used to store the memory address of an object. It uses LIFO (Last In First Out) algorithm to manage the lifetime of each variable on the Stack.

There is another stack-alike memory location that is called the Register. The (CPU)Register is a memory location where short-lived temporary instances of value type or computation values of arithmetic operations are stored. The CLR decides which short-lived memory instances are stored on either Stack or on the Register. This decision depends on the implementation of the JIT compiler.

The size of the Heap is larger than the size of the Stack and the size of the Register is smaller than the Stack.

Memory leak

If an application doesn’t free the allocated resource on memory after it is finished using it, it will create a memory leak because the same allocated memory is not being used by the application anymore. If memory leaks aren’t managed properly, the system will eventually run out of memory; consequently, the system starts giving a slow response time and the user isn’t able to close the application. The only trick is to reboot the computer!

Re-allocate (or reclaim) memory

The time during which a variable (value type or reference type) retains its value is known as its lifetime. The value of a variable may change over its lifetime. The lifetime of the variable ends when the application stops using it.

The allocated memory to store the value of a variable on the Stack is automatically freed at the end of a method. This is handled by the CLR, so we don’t have worry about it.

The allocated memory for the value of an object on the Heap are also handled by the CLR but with the help of the Garbage Collector (GC).

In a C# program an object that a variable points to becomes a candidate for garbage collection (the process of running the garbage collector to reclaim memory that is no longer accessible to the program) when the program loses its last reference to the object, either because all the references to it have been set to null or have gone out of scope.

Garbage Collection

In .NET framework, garbage collection is an automatic memory management service that takes care of the resource cleanup for all managed objects in the managed heap.

When garbage collector is initialized by the CLR, it stores and manages objects by allocating a segment of memory called the managed heap, it does this by calling a win32 VirtualAlloc method to reserve the segment of memory in managed heap and calling win32 VirtualFree method to release the segment of memory.

Each managed process in .NET has a managed heap. Each thread in a process shares the same managed heap to store and manage objects.

When garbage collector runs, it removes dead objects (object that has no reference in memory or has the value null) and reclaims their memory; it compacts the living objects together to preserve their locality and makes the managed heap smaller. The volume of allocated memory objects and the amount of survived memory objects on a managed memory heap determines how many times and for how long a garbage collector will run.

Garbage collection is a very expensive process; it doesn’t run all the time, it runs when the system runs out of physical memory or when the GC.Collect method is called manually or when allocated objects in memory need more space.

Generations

GC supports the concept of generations. It helps to organize short-lived and long-lived objects in a managed heap.

There are three generations:

  1. Generation 0
  2. Generation 1
  3. Generation 2

Generation 0

When an object is allocated on the Heap, it belongs to generation 0. It is the young generation, which contains short-lived objects like temporary variables. If newly allocated objects are larger in size, they will go on the large object heap in a generation 2 collection. GC occurs mostly in generation 0.

Generation 1

When objects survive from a garbage collection of generation 0, they go to generation 1. Objects in generation 1 serve as a buffer between short-lived and long-lived objects.

Generation 2

When objects survive from a garbage collection of generation 1, they go to generation 2. Objects in generation 2 serve as long-lived objects. If objects still survived in generation 2, they remain in generation 2 till they’re alive.

Steps involved in Garbage Collection process:

  1. Suspend all managed threads except for the thread that triggered the garbage collection.
  2. Find a list of all living objects.
  3. Remove dead objects and reclaim their memory.
  4. Compact the survived objects and promote them to an older (higher) generation.

Managed vs unmanaged resources

The GC cleans up only the managed resources (.NET Framework classes). Since the GC can not clean up unmanaged resources such as file handles, database or network connections or any other OS resource it is the task of the developer to clean up the unmanaged resources explicitly. This can be accomplished by implementing the IDisposable interface and/or using the finalizer. Both clean up unmanaged resources, but a finalizer is called by the garbage collector, and the Dispose method can be called from code.

Finalizers are also called destructors (according to MSDN). A destructor is a method with no return type and a name that includes the class’s name prefixed by ~. The GC executes an object’s destructor before permanently destroying it.

For example, the following code shows an empty destructor for the class named DisposableClass:

~DisposableClass(){}

Several rules apply to destructors that do not apply to other methods. The following list summarizes these rules:

  • Destructors can be defined in classes only, not structures.
  • A class can have at most one destructor.
  • Destructors cannot be inherited or overloaded.
  • Destructors cannot be called directly.
  • Destructors cannot have modifiers or parameters.

The GC actually calls an object’s finalizer, not its destructor. The destructor is converted into an override version of the Finalize method that executes the destructor’s code and then calls the base class’s Finalize method.

For example, suppose the Person class includes the following destructor: ~Person() { // Free unmanaged resources here. … }

This destructor is converted into the following Finalize method:

protected override void Finalize()
{
try
{
// Free unmanaged resources here. …
}
finally { base.Finalize(); }
}

You cannot explicitly override the Finalize method in C# code. That’s just as well because your code cannot call the base class’s Finalize method directly. (See the preceding list of destructor rules.)

C# finalizer is non-deterministic (non-deterministic finalization), that means you don’t know when it will be executed. It will happen only when the garbage collector determines that the object is ready for being cleaned up. But since the finalization process happens when a garbage collection occurs, you can force it by calling GC.Collect().

The garbage collector is pretty smart in managing memory, and it’s not recommended that you call GC.Collect yourself.

In release mode, the GC will see that there is no references to the StreamWriter instance so it will free the memory associated with it by calling the finalizer to release any file handles to the file.dat file.

In debug mode, the compiler will make sure that the reference isn’t garbage collected till the end of the method.

The finalizer increases the life of an object. Because the finalization code also has to run, the .NET Framework keeps a reference to the object in a special finalization queue. An additional thread runs all the finalizers at a time deemed appropriate based on the execution context. This delays garbage collection for types that have a finalizer.

The IDisposable interface

You shouldn’t depend on the GC to run a finalizer at some point in time to close your file. Instead, you should do this by yourself. To offer you the opportunity of explicitly freeing unmanaged resources, C# offers the idea of the IDisposable interface. This interface has only one method: Dispose.

The using statement

If an exception is thrown before the Dispose method call then no resource will be cleaned up. To make sure that the resource is always cleaned up C# offers the using statement.

The Dispose method is called after the using statement ends (when control goes out of the using block {}).

After disposing an object, you can’t use it any more. Using a disposed object will result in an ObjectDisposedException.

Every type that implements IDisposable should be used in a using statement whenever possible. This way you make sure that you always clean up all unmanaged resources because the using statement is translated by the compiler in a try/finally statement that calls Dispose on the object in the finally clause.

The GC calls the destructor before it permanently destroys the object so you have one last chance to clean up the object’s mess. When the destructor executes, the GC is probably in the process of destroying other objects, so the destructor’s code cannot depend on other objects existing. For example, suppose the Person class contains a reference to a Company object. The Person class’s destructor cannot assume that its Company object exists because it may have already been destroyed by the GC. That means the Person class’s destructor cannot call the Company object’s Dispose method (if it has one).

Disposable pattern

The disposable pattern is a standard way to implement the IDisposable interface so that users of your class can immediately clean up using Dispose.

The Dispose method should free both managed and unmanaged resources, but if the class has only unmanaged resources, it needs also a finalizer (C# destructor) in case the program doesn’t call Dispose.

If an object has a destructor, it must pass through a finalization queue (a queue of objects that are ready to be finalized) before it is destroyed, and that takes extra time.

If the Dispose method has already freed all the object’s resources, there’s no need to run the object’s destructor. In that case, the Dispose method can call GC.SuppressFinalize to tell the GC not to call the object’s finalizer and to let the object skip the finalization queue.

References

  • The C# Programmer’s Study Guide (MCSD) by Ali Azad & Hamza Ali
  • Exam Ref 70–483: Programming in C# by Wouter de Kort
  • MCSD Certification Toolkit (Exam 70–483) by Tiberiu Covaci, Gerry O’Brien, Rod Stephens, Vince Varallo

--

--