You are viewing a preview of this lesson. Sign in to start learning
Back to Mastering Memory Management and Garbage Collection in .NET

Heap Allocation

Managed heap structure, object layout, and reference semantics

Heap Allocation in .NET

Master heap allocation with free flashcards and spaced repetition practice. This lesson covers reference types, object lifecycle, memory regions, and performance implicationsβ€”essential concepts for understanding .NET memory management and building efficient applications.

Welcome

πŸ’» Welcome to the world of heap allocation! Understanding how .NET manages memory on the heap is crucial for writing performant, scalable applications. While the garbage collector handles most of the complexity for us, knowing what happens behind the scenes will help you make better design decisions, avoid memory leaks, and optimize your code.

In this lesson, we'll explore how reference types are allocated, what happens when objects are created, and how the heap differs from the stack. By the end, you'll have a solid foundation for understanding garbage collection and memory optimization.

Core Concepts

What is the Heap?

The heap is a region of memory used for dynamic allocation of objects that have an unpredictable lifetime. Unlike the stack, which follows a strict last-in-first-out (LIFO) order, the heap allows objects to be allocated and deallocated in any order.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚           MEMORY LAYOUT                 β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                         β”‚
β”‚  STACK (grows downward)                 β”‚
β”‚  β”œβ”€ Local variables                     β”‚
β”‚  β”œβ”€ Method parameters                   β”‚
β”‚  β”œβ”€ Return addresses                    β”‚
β”‚  └─ Value types                         β”‚
β”‚        ↓                                β”‚
β”‚        Β·                                β”‚
β”‚        Β·                                β”‚
β”‚        Β·                                β”‚
β”‚        ↑                                β”‚
β”‚  HEAP (grows upward)                    β”‚
β”‚  β”œβ”€ Reference type objects              β”‚
β”‚  β”œβ”€ Arrays                              β”‚
β”‚  β”œβ”€ Strings                             β”‚
β”‚  └─ Boxed value types                   β”‚
β”‚                                         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key characteristics of heap memory:

  • Dynamic size: Objects can be any size and are allocated at runtime
  • Managed by GC: The garbage collector automatically reclaims unused memory
  • Slower access: Heap access involves pointer dereferencing and is generally slower than stack access
  • Fragmentation: Over time, the heap can become fragmented as objects are allocated and freed

Reference Types vs Value Types

Understanding the distinction between reference types and value types is fundamental to understanding heap allocation.

Aspect Value Types Reference Types
Storage Stack (usually) Heap
Contains Actual data Reference to data
Assignment Copies the value Copies the reference
Examples int, double, struct, enum class, interface, delegate, array, string
Null Cannot be null (unless Nullable) Can be null
Cleanup Automatic (stack pop) Garbage collected

πŸ’‘ Tip: Remember the mnemonic "CLASS goes to HEAP" - Classes (reference types) are allocated on the heap, while structs (value types) typically live on the stack.

The Allocation Process

When you create a new reference type object using the new keyword, several steps occur:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   OBJECT ALLOCATION WORKFLOW           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

    var obj = new MyClass();
           |
           ↓
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ 1. Calculate     β”‚
    β”‚    object size   β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              ↓
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ 2. Find space    β”‚
    β”‚    on heap       β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              ↓
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ 3. Allocate      β”‚
    β”‚    memory block  β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              ↓
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ 4. Initialize    β”‚
    β”‚    object header β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              ↓
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ 5. Call          β”‚
    β”‚    constructor   β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              ↓
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ 6. Return        β”‚
    β”‚    reference     β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Step-by-step breakdown:

  1. Calculate size: The CLR determines how much memory is needed for the object, including its fields and overhead
  2. Find space: The allocator searches for a contiguous block of free memory on the heap
  3. Allocate block: Memory is reserved at a specific address
  4. Initialize header: Every object has a hidden header containing type information and GC metadata
  5. Call constructor: Your constructor code runs to initialize fields
  6. Return reference: A reference (memory address) is returned and stored in the variable

Object Structure in Memory

Every object on the heap has more than just your fieldsβ€”it includes important metadata:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚      OBJECT MEMORY LAYOUT           β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Object Header (8-16 bytes)         β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ Sync Block Index (4 bytes)    β”‚  β”‚
β”‚  β”‚ (for locking/hashing)         β”‚  β”‚
β”‚  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€  β”‚
β”‚  β”‚ Method Table Ptr (4-8 bytes)  β”‚  β”‚
β”‚  β”‚ (points to type info)         β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Field 1 (variable size)            β”‚
β”‚  Field 2 (variable size)            β”‚
β”‚  Field 3 (variable size)            β”‚
β”‚  ...                                β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  Padding (for alignment)            β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Components:

  • Sync Block Index: Used for thread synchronization (lock statements) and hash code storage
  • Method Table Pointer: Points to type information, enabling polymorphism and runtime type checks
  • Fields: Your actual data
  • Padding: Extra bytes to align objects on memory boundaries (typically 4 or 8 bytes)

πŸ€” Did you know? A simple object with no fields still takes at least 12 bytes on 32-bit systems (8 for header + 4 for padding) and 24 bytes on 64-bit systems!

Heap Generations

The .NET heap is divided into generations to optimize garbage collection:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚         GENERATIONAL HEAP               β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                         β”‚
β”‚  Generation 2 (old objects)             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ πŸ›οΈ Long-lived objects             β”‚  β”‚
β”‚  β”‚ Collected infrequently            β”‚  β”‚
β”‚  β”‚ (Full GC - expensive)             β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚           ↑                             β”‚
β”‚  Promoted after surviving Gen 1         β”‚
β”‚           ↑                             β”‚
β”‚  Generation 1 (medium-lived)            β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ πŸ“¦ Mid-life objects               β”‚  β”‚
β”‚  β”‚ Collected occasionally            β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚           ↑                             β”‚
β”‚  Promoted after surviving Gen 0         β”‚
β”‚           ↑                             β”‚
β”‚  Generation 0 (young objects)           β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ πŸ†• Newly allocated objects        β”‚  β”‚
β”‚  β”‚ Collected frequently              β”‚  β”‚
β”‚  β”‚ Most objects die here             β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚                                         β”‚
β”‚  Large Object Heap (LOH)                β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ 🐘 Objects β‰₯ 85,000 bytes         β”‚  β”‚
β”‚  β”‚ Collected with Gen 2              β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Why generations?

The generational hypothesis states that most objects die young. By organizing the heap into generations, the GC can focus on Gen 0 (where most garbage is) without scanning the entire heap every time.

  • Gen 0: Small, collected frequently (milliseconds)
  • Gen 1: Buffer between short and long-lived objects
  • Gen 2: Large, collected infrequently (seconds)
  • LOH: Separate space for large objects (arrays, large strings)

The Large Object Heap (LOH)

Objects β‰₯ 85,000 bytes are allocated on a special region called the Large Object Heap:

LOH characteristics:

  • No compaction: Unlike the regular heap, the LOH is not compacted by default (to avoid expensive memory moves)
  • Fragmentation risk: Can lead to fragmentation over time
  • Gen 2 collection: LOH is collected during full Gen 2 collections
  • Arrays: Large arrays (like byte[] buffers) commonly end up here

πŸ’‘ Performance tip: Reuse large objects when possible using object pooling to avoid frequent LOH allocations.

Allocation Performance

Heap allocation in .NET is surprisingly fast thanks to clever optimizations:

Bump pointer allocation:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   EFFICIENT ALLOCATION (Gen 0)          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

  Before allocation:
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ Used β”‚    ← Free Space β†’          β”‚
  β””β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β†‘β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           NextObjPtr

  After allocation:
  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
  β”‚ Used β”‚ New β”‚  ← Free Space β†’      β”‚
  β””β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”΄β”€β”€β†‘β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              NextObjPtr

  Simply increment pointer by object size!
  (Almost as fast as stack allocation)

In Gen 0, allocation is essentially just:

  1. Check if enough space remains
  2. Increment the pointer
  3. Return the old pointer value

This makes heap allocation in .NET comparable to stack allocation in performance!

⚠️ Important: This speed assumes Gen 0 has space. If Gen 0 is full, a garbage collection must occur first, which is expensive.

Examples with Explanations

Example 1: Basic Reference Type Allocation

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

public void CreatePerson()
{
    Person p1 = new Person 
    { 
        Name = "Alice", 
        Age = 30 
    };
    
    Person p2 = p1;  // Copy reference, not object
    p2.Age = 31;
    
    Console.WriteLine(p1.Age);  // Output: 31
}

What happens in memory:

  STACK                    HEAP
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ p1      │─────────→│ Person Object    β”‚
β”‚ (ref)   β”‚          β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β”‚ β”‚ Name: "Alice"β”‚ β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”‚ β”‚ Age: 31      β”‚ β”‚
β”‚ p2      │──────────→│ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ (ref)   β”‚          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜               (single object)

Explanation:

  • new Person allocates memory on the heap
  • p1 stores a reference (address) to that memory
  • p2 = p1 copies the reference, NOT the object
  • Both variables point to the same object
  • Changing p2.Age affects the same object that p1 references

Example 2: Value Type vs Reference Type Behavior

// Value type (struct)
public struct Point
{
    public int X { get; set; }
    public int Y { get; set; }
}

// Reference type (class)
public class Rectangle
{
    public int Width { get; set; }
    public int Height { get; set; }
}

public void CompareTypes()
{
    // Value type behavior
    Point pt1 = new Point { X = 10, Y = 20 };
    Point pt2 = pt1;  // Copies the entire structure
    pt2.X = 30;
    Console.WriteLine(pt1.X);  // Output: 10 (unchanged)
    
    // Reference type behavior
    Rectangle rect1 = new Rectangle { Width = 100, Height = 200 };
    Rectangle rect2 = rect1;  // Copies only the reference
    rect2.Width = 300;
    Console.WriteLine(rect1.Width);  // Output: 300 (changed!)
}

Memory layout:

VALUE TYPES (Stack):
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ pt1      β”‚     β”‚ pt2      β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β” β”‚     β”‚ β”Œβ”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ X: 10β”‚ β”‚     β”‚ β”‚ X: 30β”‚ β”‚
β”‚ β”‚ Y: 20β”‚ β”‚     β”‚ β”‚ Y: 20β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”˜ β”‚     β”‚ β””β”€β”€β”€β”€β”€β”€β”˜ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
 (two copies)

REFERENCE TYPES:
  STACK              HEAP
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ rect1    │───→│ Rectangle     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚ Width: 300    β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚ Height: 200   β”‚
β”‚ rect2    β”‚β”€β”€β”€β†’β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     (one object)

Explanation: Value types store data directly, so assignment creates an independent copy. Reference types store only a pointer, so assignment creates a shared reference to the same heap object.

Example 3: String Immutability and Heap Allocation

public void StringAllocation()
{
    string s1 = "Hello";
    string s2 = s1;
    s2 += " World";  // Creates a NEW string object
    
    Console.WriteLine(s1);  // Output: "Hello"
    Console.WriteLine(s2);  // Output: "Hello World"
}

Memory evolution:

Step 1: s1 = "Hello"
  STACK              HEAP
β”Œβ”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ s1   │──────→│ "Hello"      β”‚
β””β”€β”€β”€β”€β”€β”€β”˜       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Step 2: s2 = s1
  STACK              HEAP
β”Œβ”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ s1   │──────→│ "Hello"      β”‚
β””β”€β”€β”€β”€β”€β”€β”˜       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”Œβ”€β”€β”€β”€β”€β”€β”              ↑
β”‚ s2   β”‚β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β””β”€β”€β”€β”€β”€β”€β”˜

Step 3: s2 += " World"
  STACK              HEAP
β”Œβ”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ s1   │──────→│ "Hello"      β”‚
β””β”€β”€β”€β”€β”€β”€β”˜       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”Œβ”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ s2   │──────→│ "Hello World"β”‚ (NEW)
β””β”€β”€β”€β”€β”€β”€β”˜       β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Explanation: Strings are reference types BUT immutable. Any modification creates a new string object on the heap. The original string remains unchanged. This is why concatenating many strings in a loop is inefficientβ€”each concatenation allocates a new object.

πŸ’‘ Best practice: Use StringBuilder for repeated string modifications to avoid excessive allocations.

Example 4: Arrays and Heap Allocation

public void ArrayAllocation()
{
    // Array of value types
    int[] numbers = new int[3] { 1, 2, 3 };
    
    // Array of reference types
    Person[] people = new Person[2]
    {
        new Person { Name = "Bob", Age = 25 },
        new Person { Name = "Carol", Age = 28 }
    };
}

Memory layout:

VALUE TYPE ARRAY:
  STACK              HEAP
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ numbers │───→│ int[] (length=3) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚ β”Œβ”€β”€β”¬β”€β”€β”¬β”€β”€β”       β”‚
               β”‚ β”‚1 β”‚2 β”‚3 β”‚       β”‚
               β”‚ β””β”€β”€β”΄β”€β”€β”΄β”€β”€β”˜       β”‚
               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
            (array + data in one block)

REFERENCE TYPE ARRAY:
  STACK              HEAP
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ people │────→│ Person[] (length=2) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚ β”Œβ”€β”€β”€β”€β”¬β”€β”€β”€β”€β”         β”‚
               β”‚ β”‚ref β”‚ref β”‚         β”‚
               β”‚ β””β”€β”¬β”€β”€β”΄β”€β”¬β”€β”€β”˜         β”‚
               β””β”€β”€β”€β”Όβ”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                   ↓    ↓
            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
            β”‚ Person   β”‚ β”‚ Person   β”‚
            β”‚ Name:Bob β”‚ β”‚ Name:    β”‚
            β”‚ Age: 25  β”‚ β”‚ Carol    β”‚
            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ Age: 28  β”‚
                         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Explanation:

  • Arrays themselves are ALWAYS reference types allocated on the heap
  • Value type arrays store elements inline (contiguous memory)
  • Reference type arrays store references to separate heap objects
  • Large arrays (β‰₯85KB) go to the LOH

Common Mistakes

⚠️ Mistake 1: Assuming Assignment Creates a Copy

// WRONG ASSUMPTION
List<int> list1 = new List<int> { 1, 2, 3 };
List<int> list2 = list1;  // Doesn't copy the list!
list2.Add(4);
// list1 now also contains 4

Fix: Use explicit cloning or create a new list:

List<int> list2 = new List<int>(list1);  // Creates a copy

⚠️ Mistake 2: Creating Excessive Short-Lived Objects

// INEFFICIENT
string result = "";
for (int i = 0; i < 1000; i++)
{
    result += i.ToString();  // Creates 1000+ string objects!
}

Fix: Use StringBuilder:

var sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
{
    sb.Append(i);
}
string result = sb.ToString();

⚠️ Mistake 3: Forgetting About Boxing

// HIDDEN ALLOCATION
int number = 42;
object obj = number;  // Boxing: allocates on heap!
ArrayList list = new ArrayList();
list.Add(number);  // Also boxes!

Fix: Use generic collections to avoid boxing:

List<int> list = new List<int>();
list.Add(number);  // No boxing

⚠️ Mistake 4: Ignoring LOH Implications

// PROBLEMATIC
public byte[] ProcessData()
{
    byte[] buffer = new byte[100000];  // LOH allocation
    // Process data...
    return buffer;
}

// Called repeatedly β†’ LOH fragmentation

Fix: Use object pooling:

private static ArrayPool<byte> pool = ArrayPool<byte>.Shared;

public void ProcessData()
{
    byte[] buffer = pool.Rent(100000);
    try
    {
        // Process data...
    }
    finally
    {
        pool.Return(buffer);
    }
}

⚠️ Mistake 5: Keeping Unintended References

// MEMORY LEAK
public class EventPublisher
{
    public event EventHandler MyEvent;
}

public class Subscriber
{
    public Subscriber(EventPublisher publisher)
    {
        publisher.MyEvent += HandleEvent;  // Creates reference!
        // If not unsubscribed, Subscriber can't be GC'd
    }
    
    private void HandleEvent(object sender, EventArgs e) { }
}

Fix: Always unsubscribe:

public void Dispose()
{
    publisher.MyEvent -= HandleEvent;
}

Key Takeaways

🎯 Core Principles:

  1. Reference types live on the heap - Classes, arrays, delegates, and interfaces are heap-allocated
  2. Heap allocation is managed - The garbage collector automatically reclaims memory
  3. References are copied, not objects - Assignment copies the pointer, not the data
  4. Generations optimize collection - Gen 0 β†’ Gen 1 β†’ Gen 2 based on survival
  5. Large objects are special - Objects β‰₯85KB go to the LOH with different rules
  6. Allocation is fast - Bump pointer allocation makes heap allocation efficient in Gen 0
  7. Immutability matters - Strings create new objects on modification
  8. Boxing allocates - Converting value types to object causes heap allocation

πŸ’‘ Performance Guidelines:

  • Reuse objects when possible to reduce allocation pressure
  • Use StringBuilder for string manipulation
  • Prefer Span<T> and Memory<T> for temporary buffers
  • Pool large objects to avoid LOH fragmentation
  • Unsubscribe from events to prevent memory leaks
  • Profile before optimizingβ€”measure actual allocation impact

🧠 Memory Mnemonics:

  • CLASS = HEAP (Classes go to the heap)
  • STRUCT = STACK (Structs stay on the stackβ€”usually)
  • REF = ADDRESS (References store addresses, not data)
  • 85K = LOH (85,000 bytes triggers Large Object Heap)

πŸ“‹ Quick Reference Card

ConceptKey Point
HeapDynamic memory for reference types
Reference Typesclass, interface, delegate, array, string
Allocation CostFast in Gen 0 (bump pointer), expensive if GC needed
Object Header8-16 bytes (sync block + method table pointer)
GenerationsGen 0 (young) β†’ Gen 1 (buffer) β†’ Gen 2 (old)
LOH Thresholdβ‰₯85,000 bytes
String BehaviorReference type but immutable (creates new on modify)
BoxingValue type β†’ object causes heap allocation
GC FrequencyGen 0 (frequent) > Gen 1 (occasional) > Gen 2 (rare)
Best PracticeMinimize allocations, reuse objects, pool large buffers

πŸ“š Further Study

  1. Microsoft Docs - Memory Management: https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/memory-management-and-gc
  2. CLR via C# by Jeffrey Richter: https://www.microsoftpressstore.com/store/clr-via-c-sharp-9780735667457 (Chapter 21: The Managed Heap)
  3. Pro .NET Memory Management: https://prodotnetmemory.com/ (Comprehensive guide to .NET memory internals)

Congratulations! You now understand how heap allocation works in .NET. This knowledge will serve as the foundation for understanding garbage collection, memory optimization, and building high-performance applications. Practice identifying reference types in your code and thinking about their memory implications! πŸš€