Finalizing Objects, and Memory Concepts (Stack versus Heap)
AAHC

Click on A to make all fonts on the page smaller.

Click on A to make all fonts on the page larger.

Click on HC to toggle high contrast mode. When you move your mouse over some bold words in high contrast mode, related words are automatically highlighted. Text is shown in black and white.


In this lesson, we'll discuss topics surrounding memory, including the computer stack and heap, garbage collection, forcing memory deallocation, and class destructors.

Computer Memory
Computer Memory and Allocation

This lesson won't be an extensive discussion of the physical aspects of computer memory, but instead we'll focus on computer memory, and memory allocation, as they relate to developing software. Memory restrictions continue to become less an overall factor when writing a software application, but certain programming practices should always be considered or followed to reduce overall memory requirements, and prevent potential application failures due to errors related to memory allocation. We'll introduce these recommended practices throughout all future lessons as we introduce the related C# topic. For now, a simple guideline is to remove any unused variables and classes.

As we write computer software, we create a variety of C# components wrapped within classes that will require computer memory: methods, accessors, event handlers, variables, etc. As our program executes, memory is required to hold the code that is executing, the code that has just executed, the code to execute next, parameters passed into methods, and instantiated objects. This lesson will help you appreciate more of the technical details of memory allocation, and raise issues you should consider as you write software that creates instances of objects.

Stack Versus Heap

We've already discussed value data types and reference data types. We've learned that a value data type is stored on the stack, and that reference data types are stored on the heap. What exactly are the heap and stack?

The stack is a memory object allocated when a program begins execution, and has a variety of names: call stack, execution stack, control stack, run-time stack, program stack, etc. We'll simply continue to refer to it as the stack. A programming stack is a LIFO (last-in first-out) object, with typical functions such as push (to add to the top of the stack), and pop (to remove the top of the stack), and our stack is no different. As your program executes, the stack is created, and internal to C# and .NET, a reference is created that points to the top of the stack. Any data associated with the currently executing block of code is stored, or pushed, onto the stack as the data comes into scope. If we branch to a new section of code, we push the return reference onto the stack, and as we enter the new block of code, any data in the new block is added to the stack. As we return from our branches, we pop, or remove, the data related to where we were, and the return reference, essentially unwinding the stack contents.

The image to the right represents a typical programming stack. The stack representation includes four complete stack frames, three inactive frames, and one active, or current, frame.

Each frame, except for the first frame, includes a return reference to the previous frame, as well as any local data, and any parameters that may have been included.

We've also included an area that represents available stack space. Typically, each process within an application is given a limited amount of stack space. Should an application exceed the allocated stack space amount, an error will occur. This limited amount of space is one reason why reference data types are allocated on the heap, and not the stack.

We've also indicated the stack pointer, representing the next allocation space on the stack.

Why is the "top" of the stack at the bottom? Memory allocation can proceed in either direction within the physical memory, and we want to make sure you don't make the assumption that the "top" of the stack is always at the top.

The heap is memory allocated "on the fly" when the new operator is called, or with reference data types such as strings. Potentially, the heap could grow as big as needed, only limited by the physical and virtual memory of your computer. For C# applications, the heap is actually a "managed" heap, meaning that all heap allocations and deallocations are tracked by the .NET architecture. We'll continue to refer to the C# managed heap as simply the heap.

When objects are created on the heap, we can create multiple references to the object. As each additional reference is created, internally a reference counter is incremented. So long as at least one reference to an allocated heap object exists within scope, that object will remain on the heap. When the reference counter reaches zero, then that allocated heap object may be removed by the .NET Garbage Collector, which we'll discuss more shortly.

We've summarized the differences between the stack and the heap in the table below.

Type of MemoryData TypeItem OrderItem LifetimeItem AddItem RemovalRemoval Timing
StackValue Data TypesSequential (LIFO)Execution Block ScopePushPopDeterministic (upon Pop)
HeapReference Data TypesRandomReference Countnew OperatorGarbage CollectionNon-Deterministic

As you review the differences between the stack and the heap, note that heap memory allocation is random, unlike the stack, where the memory allocated is in a sequential order. Random memory allocation means that when an object is instantiated on the heap, it could be anywhere, and as the memory is deallocated, blocks of previously allocated memory will be cleared, potentially leaving blocks of allocated memory, then unallocated memory, followed again by allocated memory.

Garbage Collection

The .NET managed heap tracks memory allocations, and when the references to the memory reach zero, .NET employs a mechanism known as the garbage collector to free up previously allocated memory. In the table listing the differences between the stack and the heap, we indicated that heap memory deallocation is non-deterministic, meaning that even though your code may no longer reference a block of heap memory, the actual deallocation of that memory may be delayed. If you're interested to know more about how the garbage collector works, and how it determines what memory is deallocated when, see the Microsoft MSDN article Object Lifetime View.

Sometimes, you want to control how an object is deallocated, or perform some amount of cleanup just before destroying an object. We'll discuss this topic next.

Finalizing Objects and Destructors

As we've discussed, the .NET garbage collector will deallocate managed resources, but what about unmanaged resources that we may use, such as an open file connection, or a database connection? We may want to maintain such connections throughout the lifetime of an object, only releasing the connections and destroying any objects related to the connections when the object is finally destroyed. The destruction of unmanaged resources is accomplished as part of finalizing an object, and in C# is managed in the class destructor.

Class destructor? Yes, that's right, just as a class has a constructor, it may also have a destructor. Syntactically, a destructor is almost identical to a constructor, except that you include a tilde (~) before the class name. Destructors, just like constructors, do not return a value; however, unlike constructors, destructors do not take parameters. Purely static classes cannot include a destructor.

Class Destructor
public class Employee
{
    public Employee()
    {
        // Empty constructor
    }
    
    public ~Employee()
    {
        // Destructor
    }
}                

Alright, let's do some coding!

Coding

Let's explore ways to look at memory usage using the .NET System.GC class. We'll use the GC GetTotalMemory method to ask the garbage collector for an approximation of how much memory is currently allocated and thus managed. Why an approximation? The garbage collector is heavily optimized to prevent impacting the performance of an application, so memory flagged for deallocation may not yet be reclaimed. We will pass in true as a parameter to GetTotalMemory to ask the garbage collector to take additional time to try to more accurately calculate allocated memory. Typically, as OOP programmers using C#, we let the garbage collector deal with all of our managed memory requests, and would not call the GetTotalMemory method, except in our case to explore memory usage in .NET. After peeking into memory allocation, we'll use the .NET Diagnostics class to look into constructors and destructors by creating a class that uses a StopWatch object to monitor how long an object is instantiated (created) then destroyed. This simple class will contain a List field (a public member variable) of data type object, which means that we can add any data type that is based on the object class. Specifically, we're going to create a large byte array to make sure we take a lot of space. We'll be covering arrays in a later lesson, so for now you'll just need to enter the code related to the byte array as it is presented in the lesson. You should note, though, that we use the new keyword with our byte array, so the memory allocated is from the heap, and is managed by the .NET garbage collector.

Select File | New | Project. Change the project name to Destructors and click OK.

Click the entry for Form1.cs and change it to Destructors.cs. Change the form title bar's Text property in the Properties Window to Destructors. Click to save your changes.

Modify the form to look like this:

Change each control's Name property to match the Name Property column below, and Text property to match the Text Property column. Arrange the controls similar to the image. When you finish, click to save your changes.

ObjectName PropertyText Property
ListBoxoutputListBox 
ButtoncreateButtonCreate
ButtondestroyButtonDestroy
ButtonexitButtonExit

Double-click each of the createButton, destroyButton, and exitButton controls to automatically generate the default event handler code.

Add an event handler for the Destructors Form Shown event by selecting the Form (clicking on the Form title bar), clicking the Events icon to view the Events Properties Window, and double-clicking the Shown event.

Modify the Destructors.cs code as shown below.

Destructors.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace Destructors
{
    public partial class Destructors : Form
    {
        public Destructors()
        {
            InitializeComponent();
        }
        
        private void createButton_Click(object sender, EventArgs e)
        {
        }
        
        private void destroyButton_Click(object sender, EventArgs e)
        {
        }
        
        private void exitButton_Click(object sender, EventArgs e)
        {
            this.Close();
        }
        
        private void Destructors_Shown(object sender, EventArgs e)
        {
            outputListBox.Items.Add("Start: GC Memory (bytes): " + GC.GetTotalMemory(true).ToString());
            outputListBox.Items.Add("");
            string testString = "This is a string that requires memory allocation on the heap";
            outputListBox.Items.Add("String 1: GC Memory (bytes): " + GC.GetTotalMemory(true).ToString());
            outputListBox.Items.Add("");
            testString += "Changing the string value results in deallocation of original heap memory, and allocation of new memory";
            outputListBox.Items.Add("String 2: GC Memory (bytes): " + GC.GetTotalMemory(true).ToString());
            outputListBox.Items.Add("");
        }
    }
}

Click and to save and run the program. You should see something like this (your memory values may be different):

Click Exit to close the program. Let's discuss how it works.

OBSERVE: Destructors.cs
.
.
.
private void Destructors_Shown(object sender, EventArgs e)
{
    outputListBox.Items.Add("Start: GC Memory (bytes): " + GC.GetTotalMemory(true).ToString());
    outputListBox.Items.Add("");
    string testString = "This is a string that requires memory allocation on the heap";
    outputListBox.Items.Add("String 1: GC Memory (bytes): " + GC.GetTotalMemory(true).ToString());
    outputListBox.Items.Add("");
    testString += "Changing the string value results in deallocation of original heap memory, and allocation of new memory";
    outputListBox.Items.Add("String 2: GC Memory (bytes): " + GC.GetTotalMemory(true).ToString());
    outputListBox.Items.Add("");
}
.
.
.

We display the amount of memory allocated by the GetTotalMemory method of the GC object. We next declare and initialize testString. As a string, testString is a reference data type stored on the stack, that references (or points) to memory on the heap where the actual string contents are stored. Your output will show an increase in the managed memory reported by the garbage collector. We next concatenate additional text to our testString variable, and as the text indicates, when we change the value of a string, we have to allocate new memory to hold the new string contents, and deallocate the original memory. The content of the testString reference value is changed to reference the new memory location, and the previously allocated memory is flagged for eventual deallocation by the garbage collector.

Now, let's add a new class so we can take a look at a destructor. We'll name it "Bag," a term that is frequently used in programming to refer to an object that holds another object.

Right-click the Destructors project item in the Solution Explorer, and select Add | Class.... In the Add New Item dialog box, change the Name from Class1.cs to Bag.cs, and then click Add.

In the Bag.cs Code Editor, modify the code as shown below.

Bag.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace Destructors
{
    class Bag
    {
        public List<object> Items { get; set; }
        
        private Stopwatch _stopWatch = new Stopwatch();
        
        public Bag()
        {
            _stopWatch.Start();
            Console.WriteLine("Bag constructor, start stop watch");
            Items = new List<object>();
        }
        
        ~Bag()
        {
            _stopWatch.Stop();
            Console.WriteLine("Bag destructor, stop watch stopped, object existed for " +
                _stopWatch.ElapsedMilliseconds / 1000 + " seconds");
        }
    }
}

Click to save your changes. Before we run it, we'll add code to Destructors.cs to instantiate the Bag.cs class, but first let's discuss the code we've added.

OBSERVE: Bag.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace Destructors
{
    class Bag
    {
        public List<object> Items { get; set; }
        
        private Stopwatch _stopWatch = new Stopwatch();
        
        public Bag()
        {
            // Constructor
            _stopWatch.Start();
            Console.WriteLine("Bag constructor, start stopwatch");
            Items = new List<object>();
        }
        
        ~Bag()
        {
            // Destructor
            _stopWatch.Stop();
            Console.WriteLine("Bag destructor, stopwatch stopped, object existed for " +
                _stopWatch.ElapsedMilliseconds / 1000 + " seconds");
        }
    }
}

We've declared a public List automatic property field. By making this field public, we explicitly expose our List methods, making it easy to manipulate the Items class field. We've also added the _stopWatch object of type Stopwatch from the Diagnostics class, giving us stopwatch timing capability. In the constructor, we start our _stopWatch, and create our Items List object. When our object is eventually destroyed (when the garbage collector reclaims the memory used by the instance of the Bag class), the Bag destructor is called, and our _stopWatch will be stopped. We'll use the ElapsedMilliseconds method, dividing by 1000 to convert to seconds, to display how much time elapsed between when the instance of Bag was created, and then reclaimed, by the garbage collector.

Now let's create an instance of the Bag class.

Modify the Destructors.cs code as shown below.

Destructors.cs
.
.
.
        private Bag _bag = null;
        
        public Destructors()
        {
            InitializeComponent();
        }
        
        private void createButton_Click(object sender, EventArgs e)
        {
            // Only create a single Bag.
            if (_bag == null)
            {
                // Create a new Bag instance.
                _bag = new Bag();
                
                // Create a variable that requires allocating a large block of heap memory.
                byte[] byteArray = new byte[100000];
                for (int i = 0; i < byteArray.Length; i++)
                {
                    byteArray[i] = 1;
                }
                
                // Add the variable to our Bag.
                _bag.Items.Add(byteArray);
                
                outputListBox.Items.Add("Create: GC Memory (bytes): " + GC.GetTotalMemory(true).ToString());
                outputListBox.Items.Add("");
            }
        }
        
        private void destroyButton_Click(object sender, EventArgs e)
        {
            // Make sure Bag has been created.
            if (_bag != null)
            {
                // Remove the reference to our Bag allocated memory.
                _bag = null;
                
                outputListBox.Items.Add("Destroy: GC Memory (bytes): " + GC.GetTotalMemory(true).ToString());
                outputListBox.Items.Add("");
            }
        }
.
.
.

Click and to save and run the program. Click Create, and then click Destroy. You should see something like this (again, your memory values may be different):


Let's discuss how this code works.

Destructors.cs
.
.
.
private Bag _bag = null;

public Destructors()
{
    InitializeComponent();
}

private void createButton_Click(object sender, EventArgs e)
{
    // Only create a single Bag.
    if (_bag == null)
    {
        // Create a new Bag instance.
        _bag = new Bag();
        
        // Create a variable that requires allocating a large block of heap memory.
        byte[] byteArray = new byte[100000];
        for (int i = 0; i < byteArray.Length; i++)
        {
            byteArray[i] = 1;
        }
        
        // Add the variable to our Bag.
        _bag.Items.Add(byteArray);
        
        outputListBox.Items.Add("Create: GC Memory (bytes): " + GC.GetTotalMemory(true).ToString());
        outputListBox.Items.Add("");
    }
}

private void destroyButton_Click(object sender, EventArgs e)
{
    // Make sure Bag has been created.
    if (_bag != null)
    {
        _bag = null;
        // Remove the reference to our Bag allocated memory.
        
        outputListBox.Items.Add("Destroy: GC Memory (bytes): " + GC.GetTotalMemory(true).ToString());
        outputListBox.Items.Add("");
    }
}
.
.
.

We declare a private class variable _bag from our Bag class. In our createButton Click event handler, we create a byte array variable byteArray that allocates a large block of heap memory. After creating the byte array, we add byteArray to our _bag instance. The allocation of the large block of heap memory is reflected in the amount of managed memory reported by the garbage collector.

In the destroyButton Click event handler, we set our _bag reference to null. From our earlier discussion, you should note that the _bag variable, stored on the stack, no longer references, or points to, the allocated memory. By setting the _bag reference variable to null, the garbage collector may reclaim this memory. We report the amount of managed memory, which should have decreased if the garbage collector did reclaim the memory.

So, that's our examination of stack and heap memory, and using a destructor. We'll revisit the destructor later when we deal with unmanaged memory and resources.

Before you move on to the next lesson, do your homework! Right-click in the window where this lesson text appears and select Back. Then select Quiz for this lesson in the syllabus and answer the quiz questions. When you finish the quiz questions, click HAND IT IN at the bottom of that window. Then do the same with the Project(s) for the lesson. Your instructor will grade your quiz(zes) and project(s) and provide guidance if needed.