DEV Community

mohamed Tayel
mohamed Tayel

Posted on

Mastering C# Fundamentals :Memory Management and Garbage Collection

Meta Description: Learn the essentials of memory management in C# with a focus on garbage collection. Discover how stack and heap work, visualize unreachable objects, and explore hands-on assignments to better understand garbage collection using Visual Studio Diagnostic Tools. This is the first in a series on mastering memory management in .NET.

Note: Memory management is a very broad topic that cannot be covered comprehensively in a single article. This is the first in a series of articles aimed at exploring different aspects of memory management, so stay tuned for more in-depth discussions.

When developing applications in C#, managing memory efficiently is crucial. Objects need to be properly allocated and deallocated in memory to ensure the smooth running of your software. But what happens when objects are no longer needed, and their memory is still occupied? This is where garbage collection comes in.

In this article, we'll explore memory management in C#, including how objects are stored in memory, and we'll introduce garbage collection as the process that automatically cleans up unused objects. We'll illustrate these concepts using simple examples, diagrams, and assignments for different experience levels. Additionally, we'll show you how to use Visual Studio Diagnostic Tools to observe garbage collection in action.

The Stack and Heap: Where Is Memory Stored?

When we create an object in C#, it’s allocated in memory, but where exactly? In .NET, memory is divided into two main areas: the stack and the heap.

  • The stack is where value types and references to objects are stored.
  • The heap is where the actual objects live, and their references are stored on the stack.

The following diagram explains the difference between these two types of memory.

Stack and Heap Memory
In this diagram, we have three objects: o1, o2, and o3. Each object is created using new, and the references (o1, o2, o3) are stored on the stack, while the actual object data is stored in the heap.

Notice that each reference points to an object in the heap. When a reference is removed or set to null, the object remains in the heap, but it becomes unreachable. In the case of o3, its reference has been broken, which means it's eligible for garbage collection.

The Problem of Unreachable Objects

So what happens if we don't manually remove these unreachable objects? As we keep creating new objects and references, memory consumption will increase. Since memory is limited, this can eventually cause our application to run out of memory and crash.

In C#, unreachable objects are a common phenomenon:

  • When a reference is removed or a variable goes out of scope, the object becomes orphaned—it still exists in memory, but nothing is pointing to it.
  • These orphaned objects are called "zombie objects" if not properly cleaned up.

Here's an illustration to help you visualize what happens to unreachable objects.

Unreachable Objects and Garbage Collection
In this diagram, you can see multiple objects in the heap, some of which no longer have any references pointing to them. These are the objects marked with the warning symbol. When an object becomes unreachable, it becomes a target for garbage collection.

How Garbage Collection Works

Garbage collection (GC) is a mechanism in .NET that automatically manages memory by cleaning up unreachable objects. It’s a background process that runs at specific points in time when the runtime decides that memory needs to be freed up.

Key Points About Garbage Collection:
  • The garbage collector identifies objects that are no longer reachable from the stack and removes them from memory.
  • This process helps free up space for new objects, preventing memory leaks and ensuring efficient memory usage.

Example: Creating and Cleaning Objects

Let's look at an example to demonstrate the concept of garbage collection in action. We'll create a list of Book objects, clear the list, and then trigger garbage collection manually to illustrate what happens.

public class Book
{
    public string Title { get; set; }
    public string Author { get; set; }

    public Book(string title, string author)
    {
        Title = title;
        Author = author;
    }
}

public static void MediumGarbageCollection()
{
    List<Book> books = new List<Book>();

    // Adding books to the list
    for (int i = 0; i < 50000; i++)
    {
        books.Add(new Book($"Title {i}", $"Author {i}"));
    }

    Console.WriteLine("Books created.");

    // Remove the references to the books by clearing the list
    books = null;

    // Force garbage collection
    GC.Collect();

    Console.WriteLine("Garbage collection triggered for Book objects.");
}
Enter fullscreen mode Exit fullscreen mode

In this example:

  1. We create a list of 50,000 Book objects.
  2. We clear the list by setting books to null, which means all the books in that list are now unreachable.
  3. Finally, we manually call GC.Collect() to trigger garbage collection.

Using Diagnostic Tools to Observe Garbage Collection

Garbage collection is usually a background task that runs without you noticing it. However, in Visual Studio, you can observe garbage collection in action using the Diagnostic Tools window. Here’s how:

  1. Set Up Your Project:

    • Use one of the example assignments provided below, such as the Medium Level Example (Book objects).
  2. Add Breakpoints:

    • Set breakpoints in strategic places in your code to observe memory before and after clearing references and triggering garbage collection.
    • In the above code, set a breakpoint after the line Console.WriteLine("Books created.");, and another one after Console.WriteLine("Garbage collection triggered for Book objects.");.
  3. Run in Debug Mode:

    • Press F5 to run the application in Debug Mode.
    • This will allow you to pause at each breakpoint and observe memory usage.
  4. Open Diagnostic Tools:

    • If the Diagnostic Tools window is not already open, go to Debug > Windows > Show Diagnostic Tools.
    • You will see metrics for CPU Usage, Memory Usage, and a timeline view of the application.
  5. Take Memory Snapshots:

    • While paused at a breakpoint, click Take Snapshot in the Memory Usage section of Diagnostic Tools.
    • Continue running (F5) until you reach the next breakpoint, and take another snapshot.
    • Compare the two snapshots to observe how the memory usage has changed before and after garbage collection.
  6. Look for Garbage Collection Events:

    • In the timeline, look for yellow markers, which represent garbage collection events that .NET triggered.
    • If you manually called GC.Collect(), you should see a significant drop in memory usage as unreachable objects are cleared.

Assignments

Below, we'll create three different assignments: Easy, Medium, and Difficult. Each level involves managing memory and garbage collection in different scenarios to help you better understand these concepts.

Assignment 1: Easy Level - Car Objects

Create and add Car objects to a list, then clear the list and observe how garbage collection works.

public class Car
{
    public string Model { get; set; }
    public int Year { get; set; }

    public Car(string model, int year)
    {
        Model = model;
        Year = year;
    }
}

public static void EasyGarbageCollection()
{
    List<Car> cars = new List<Car>();

    // Adding cars to the list
    for (int i = 0; i < 10000; i++)
    {
        cars.Add(new Car($"Model{i}", 2000 + i));
    }

    // Clear the list and set it to null to make objects eligible for garbage collection
    cars.Clear();
    cars = null;

    // Force garbage collection
    GC.Collect();

    Console.WriteLine("Garbage collection triggered for Car objects.");
}
Enter fullscreen mode Exit fullscreen mode

Task:

  • Run the above code and use Visual Studio's Diagnostic Tools to observe the memory usage before and after GC.Collect().
  • Explain what you see and why it happens.
Assignment 2: Medium Level - Book Objects

Create a list of Book objects, remove half of the objects, and force garbage collection.

public static void MediumGarbageCollection()
{
    List<Book> books = new List<Book>();

    // Adding books to the list
    for (int i = 0; i < 50000; i++)
    {
        books.Add(new Book($"Title {i}", $"Author {i}"));
    }

    Console.WriteLine("Books created.");

    // Removing half of the books from the list
    books.RemoveRange(0, books.Count / 2);

    // Set the list to null
    books = null;

    // Force garbage collection
    GC.Collect();

    Console.WriteLine("Garbage collection triggered for Book objects.");
}
Enter fullscreen mode Exit fullscreen mode

Task:

  • Run the above code, and take memory snapshots before and after clearing the list.
  • Observe what happens to memory usage as you remove half of the books and trigger garbage collection.
Assignment 3: Difficult Level - Orders and Customers with References

Create Order objects that have references to Customer objects. Remove these references and force garbage collection

.

public class Order
{
    public int OrderId { get; set; }
    public string ProductName { get; set; }
    public Customer Customer { get; set; }

    public Order(int orderId, string productName, Customer customer)
    {
        OrderId = orderId;
        ProductName = productName;
        Customer = customer;
    }
}

public class Customer
{
    public int CustomerId { get; set; }
    public string Name { get; set; }

    public Customer(int customerId, string name)
    {
        CustomerId = customerId;
        Name = name;
    }
}

public static void DifficultGarbageCollection()
{
    List<Order> orders = new List<Order>();
    List<Customer> customers = new List<Customer>();

    // Creating customers
    for (int i = 0; i < 100000; i++)
    {
        customers.Add(new Customer(i, $"Customer {i}"));
    }

    // Creating orders and assigning customers
    for (int i = 0; i < 100000; i++)
    {
        orders.Add(new Order(i, $"Product {i}", customers[i]));
    }

    // Breaking links between orders and customers
    foreach (var order in orders)
    {
        order.Customer = null;
    }

    // Set the list of orders and customers to null
    orders = null;
    customers = null;

    // Force garbage collection
    GC.Collect();

    Console.WriteLine("Garbage collection triggered for Order and Customer objects.");
}
Enter fullscreen mode Exit fullscreen mode

Task:

  • Use the Diagnostic Tools to observe how memory is allocated and released after breaking the references between Order and Customer objects.
  • Explain why breaking the links is essential for enabling garbage collection to remove these objects from memory.

Conclusion

Understanding how memory is allocated in the stack and heap and how garbage collection works is crucial for efficient C# programming. Garbage collection helps ensure that memory is automatically managed, preventing the issues that come with having unreachable zombie objects sitting around in the heap.

The diagrams we used illustrate how references to objects are stored and what happens when they are broken, making it easy to understand why garbage collection is necessary. Using Visual Studio Diagnostic Tools, you can observe this process in real-time, giving you valuable insight into memory usage in your application.

Note: Memory management is a vast topic, and there are many more aspects that we couldn’t cover in just this article. This is just the beginning—stay tuned as we continue this series on memory management, where we’ll explore more advanced topics and best practices.

Try out the assignments to get hands-on experience with garbage collection and observe how it works under different scenarios. By understanding these concepts, you can write better, more memory-efficient code and have more confidence in your application’s performance.

Top comments (0)