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

Memory Fundamentals

Understanding stack vs heap allocation, memory layout, and lifetime semantics in .NET runtime

Memory Fundamentals

Master .NET memory management with free flashcards and spaced repetition practice. This lesson covers stack and heap allocation, value types versus reference types, and memory architectureβ€”essential concepts for building high-performance applications and understanding garbage collection.

Welcome to Memory Fundamentals πŸ’»

Memory management is the foundation of software performance. Every variable you declare, every object you instantiate, and every method you call involves memory. Understanding how .NET organizes and manages memory will transform you from a developer who writes code that "just works" into one who writes efficient, scalable applications.

In this lesson, you'll discover the architecture underlying .NET's memory system, learn the critical differences between stack and heap allocation, and understand why certain operations are lightning-fast while others require garbage collection. These fundamentals form the bedrock for advanced topics like performance optimization and memory leak prevention.

Core Concepts: The Memory Landscape πŸ—ΊοΈ

The Two Memory Regions

.NET divides memory into two primary regions, each optimized for different purposes:

Memory Region Purpose Speed Size Management
Stack Method execution, local variables ⚑ Very Fast ~1 MB (small) Automatic (LIFO)
Heap Objects, dynamic data 🐒 Slower GB+ (large) Garbage Collector

The Stack operates like a stack of platesβ€”you can only add (push) or remove (pop) from the top. When a method executes, its local variables are pushed onto the stack. When the method returns, those variables are popped off automatically. This LIFO (Last In, First Out) behavior makes stack allocation incredibly fastβ€”just increment or decrement a pointer.

The Heap is a large, flexible memory pool where objects live. Unlike the stack's orderly nature, the heap allows allocation and deallocation in any order. This flexibility comes at a cost: the runtime must track which memory is in use and periodically clean up (garbage collect) abandoned objects.

Visual Memory Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚           .NET MEMORY LAYOUT                β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                             β”‚
β”‚  STACK (per thread)          HEAP (shared)  β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”          β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”β”‚
β”‚  β”‚ Method Frame  β”‚          β”‚  Object A   β”‚β”‚
β”‚  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€          β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”‚
β”‚  β”‚ Local Vars    β”‚ ────────>β”‚  Object B   β”‚β”‚
β”‚  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€   ref    β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”‚
β”‚  β”‚ Parameters    β”‚          β”‚  Object C   β”‚β”‚
β”‚  β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€          β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”‚
β”‚  β”‚ Return Addr   β”‚          β”‚  (free)     β”‚β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜β”‚
β”‚       ↓↑ Push/Pop             GC Managed   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ’‘ Memory Tip: Think of the stack as your desk (limited space, things you're working on right now) and the heap as your warehouse (massive storage, but requires inventory management).

Value Types vs Reference Types

This distinction is fundamental to understanding .NET memory behavior:

Value Types store their data directly where they're declared:

  • Primitives: int, double, bool, char, byte, etc.
  • Structs: DateTime, Guid, custom structs
  • Enums

When declared as local variables, value types live on the stack. When declared as fields of a class, they live inline within that object on the heap.

Reference Types store a reference (memory address) to data on the heap:

  • Classes: string, object, custom classes
  • Arrays: int[], string[]
  • Delegates and interfaces

The reference itself (a pointer) might be on the stack, but the actual object data always lives on the heap.

VALUE TYPE BEHAVIOR:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Stack        β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ x = 42       β”‚  Value stored directly
β”‚ y = 99       β”‚  Each variable independent
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

REFERENCE TYPE BEHAVIOR:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”        β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Stack        β”‚        β”‚ Heap         β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€        β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ obj1 ──────────────> β”‚ {data: 42}   β”‚
β”‚              β”‚        β”‚              β”‚
β”‚ obj2 ──────────────> β”‚ {data: 99}   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜        β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
     References point to heap objects

Memory Allocation Deep Dive

When you write this code:

int count = 5;                    // Value type
string name = "Alice";            // Reference type
Person person = new Person();     // Reference type

Here's what happens in memory:

Declaration Stack Allocation Heap Allocation
int count = 5 βœ… Value 5 stored directly ❌ None
string name = "Alice" βœ… Reference address βœ… String object "Alice"
Person person = new Person() βœ… Reference address βœ… Person object with all fields

Stack allocation is essentially freeβ€”just move a pointer. No garbage collection needed since stack frames are automatically cleaned up when methods return.

Heap allocation involves:

  1. Finding free memory space (can be complex)
  2. Initializing the object
  3. Returning a reference
  4. Eventually, garbage collection to reclaim space

πŸ” Did you know? String literals are internedβ€”identical string literals in your code share the same heap object to save memory!

Copying Behavior: A Critical Difference

The value/reference distinction profoundly affects how data is copied:

Value type copying creates an independent duplicate:

int x = 10;
int y = x;     // y gets its own copy of 10
y = 20;        // x is still 10, y is now 20

Reference type copying copies the reference, not the object:

Person person1 = new Person { Age = 30 };
Person person2 = person1;    // Both point to same object!
person2.Age = 40;            // person1.Age is now 40 too!
VALUE TYPE COPY:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ x = 10   β”‚ copy  β”‚ y = 10   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  ───> β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  Independent         Independent

REFERENCE TYPE COPY:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ person1  β”‚       β”‚ person2  β”‚
β”‚   β”‚      β”‚ copy  β”‚   β”‚      β”‚
β”‚   ↓      β”‚  ───> β”‚   ↓      β”‚
β””β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”˜       β””β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”˜
    β”‚                  β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              ↓
         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
         β”‚{Age:40} β”‚  Same object!
         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

⚠️ Common Mistake: Assuming assignment creates a copy of an object. For reference types, it creates another reference to the same object.

Memory Size and Layout πŸ“

Value Type Sizes

Value types have predictable sizes:

Type Size (bytes) Range/Notes
bool 1 true/false
byte 1 0 to 255
char 2 Unicode character
short 2 -32,768 to 32,767
int 4 Β±2.1 billion
long 8 Β±9.2 quintillion
float 4 Single precision
double 8 Double precision
decimal 16 High precision, financial
DateTime 8 Struct (ticks + kind)
Guid 16 128-bit identifier

Reference Type Overhead

Every object on the heap has overhead beyond its field data:

OBJECT MEMORY LAYOUT (64-bit .NET):
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Object Header (8 bytes)         β”‚  Sync block, hash code
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Method Table Ptr (8 bytes)      β”‚  Type information
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Field Data (variable)           β”‚  Your actual data
β”‚ ...                             β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
   Minimum object size: 24 bytes

This means even an empty class instance consumes 16 bytes of overhead (plus padding to align to 8-byte boundaries).

πŸ’‘ Performance Insight: For small data structures, structs can be more memory-efficient than classes because they avoid heap overhead. But be cautiousβ€”large structs get copied on every assignment or parameter pass!

Struct Packing and Alignment

The CLR aligns fields to memory boundaries for CPU efficiency:

struct BadLayout
{
    byte a;      // 1 byte
    int b;       // 4 bytes (needs 4-byte alignment)
    byte c;      // 1 byte
    long d;      // 8 bytes (needs 8-byte alignment)
}
// Actual size: 24 bytes (due to padding)

struct GoodLayout
{
    long d;      // 8 bytes
    int b;       // 4 bytes
    byte a;      // 1 byte
    byte c;      // 1 byte
}
// Actual size: 16 bytes (better packing)
BAD LAYOUT (24 bytes with padding):
β”Œβ”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”¬β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚a β”‚PADβ”‚ b β”‚c β”‚   PAD    β”‚ d (8 bytes)
β”‚1 β”‚ 3 β”‚ 4 β”‚1 β”‚    3     β”‚
β””β”€β”€β”΄β”€β”€β”€β”΄β”€β”€β”΄β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

GOOD LAYOUT (16 bytes, minimal padding):
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”¬β”€β”€β”¬β”€β”€β”¬β”€β”€β”
β”‚    d     β”‚ b  β”‚a β”‚c β”‚PDβ”‚
β”‚ 8 bytes  β”‚ 4  β”‚1 β”‚1 β”‚ 2β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”΄β”€β”€β”΄β”€β”€β”΄β”€β”€β”˜

🧠 Memory Device: "BIG to small, saves it all"β€”declare larger types first to minimize padding.

Example 1: Stack vs Heap in Action

Let's trace memory allocation through a method call:

public class Calculator
{
    public int Multiply(int a, int b)
    {
        int result = a * b;
        return result;
    }
}

public void ProcessNumbers()
{
    int x = 5;
    int y = 10;
    Calculator calc = new Calculator();
    int answer = calc.Multiply(x, y);
}

Step-by-step memory allocation:

Step Stack State Heap State
1. Enter ProcessNumbers() Stack frame created -
2. int x = 5 x: 5 -
3. int y = 10 x: 5, y: 10 -
4. new Calculator() calc: [reference] Calculator object allocated
5. Enter Multiply() New frame: a: 5, b: 10 -
6. int result = ... result: 50 -
7. Return from Multiply() Frame popped, value copied -
8. answer = ... answer: 50 -
9. Exit ProcessNumbers() Frame destroyed Calculator object marked for GC

Key observations:

  • All integers (x, y, a, b, result, answer) never touch the heap
  • Only the Calculator object requires heap allocation
  • Stack cleanup is automatic and instantaneous
  • The Calculator object remains in heap until garbage collected

Example 2: Reference Aliasing Problem

This common scenario demonstrates why understanding references matters:

public class ShoppingCart
{
    public List<string> Items { get; set; } = new List<string>();
}

public void ProcessOrder()
{
    ShoppingCart cart1 = new ShoppingCart();
    cart1.Items.Add("Laptop");
    cart1.Items.Add("Mouse");
    
    // Trying to "backup" the cart
    ShoppingCart backup = cart1;  // ⚠️ Just copies the reference!
    
    // Clear the cart for next customer
    cart1.Items.Clear();
    
    // Oops! backup.Items is also empty now!
    Console.WriteLine($"Backup items: {backup.Items.Count}");  // Prints: 0
}

Memory visualization:

AFTER: backup = cart1

  Stack                     Heap
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ cart1 ───┼────────>β”‚ ShoppingCart    β”‚
β”‚          β”‚         β”‚  Items ────┐    β”‚
β”‚ backup ──┼────┐    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”˜
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚                 β”‚
                β”‚                 ↓
                β”‚         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
                └────────>β”‚ Listβ”‚
                          β”‚ [Laptop]    β”‚
                          β”‚ [Mouse]     β”‚
                          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
          Both references point to same objects!

The fixβ€”create a true copy:

// Shallow copy (new ShoppingCart, but same List reference)
ShoppingCart backup = new ShoppingCart 
{ 
    Items = cart1.Items  // Still shares the List!
};

// Deep copy (completely independent)
ShoppingCart backup = new ShoppingCart();
backup.Items.AddRange(cart1.Items);  // New List, copied items

Example 3: Boxing and Unboxing Overhead

Boxing occurs when a value type is converted to object or an interfaceβ€”it must be wrapped in a heap-allocated object:

// Boxing: value type β†’ reference type
int number = 42;           // Stack: 4 bytes
object boxed = number;     // Heap: 24+ bytes (object overhead + value)

// Unboxing: reference type β†’ value type
int unboxed = (int)boxed;  // Copies value back to stack
BOXING PROCESS:

Stack              Heap
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ number  β”‚
β”‚   42    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
     β”‚
     β”‚ Boxing creates heap object
     ↓
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ boxed ──┼────>β”‚ Object Header    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β”‚ Type: Int32      β”‚
                β”‚ Value: 42        β”‚
                β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
              24 bytes for 4 bytes of data!

Performance impact in loops:

// BAD: Boxing on every iteration
ArrayList list = new ArrayList();  // Stores objects
for (int i = 0; i < 10000; i++)
{
    list.Add(i);  // ⚠️ Boxes each int! 10,000 heap allocations!
}

// GOOD: No boxing with generics
List<int> list = new List<int>();  // Stores ints directly
for (int i = 0; i < 10000; i++)
{
    list.Add(i);  // βœ… No boxing! Values stored inline in array
}

πŸ’‘ Performance Tip: Generic collections (List<T>, Dictionary<TKey, TValue>) avoid boxing by storing value types directly in their internal arrays.

Example 4: Memory Layout of Complex Objects

Let's examine how nested objects are actually laid out:

public class Department
{
    public int Id;                      // 4 bytes
    public string Name;                 // 8 bytes (reference)
    public DateTime CreatedDate;        // 8 bytes (struct, inline)
}

public class Employee
{
    public int EmployeeId;              // 4 bytes
    public string FirstName;            // 8 bytes (reference)
    public string LastName;             // 8 bytes (reference)
    public Department Department;       // 8 bytes (reference)
    public decimal Salary;              // 16 bytes (struct, inline)
}

Memory layout:

Employee Object in Heap:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Object Header        (8 bytes)  β”‚
β”‚ Method Table Ptr     (8 bytes)  β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ EmployeeId           (4 bytes)  β”‚
β”‚ [padding]            (4 bytes)  β”‚  Alignment
β”‚ FirstName ref        (8 bytes)  β”œβ”€β”€β”€> "John" in heap
β”‚ LastName ref         (8 bytes)  β”œβ”€β”€β”€> "Smith" in heap
β”‚ Department ref       (8 bytes)  β”œβ”€β”€β”€> Department object
β”‚ Salary (decimal)     (16 bytes) β”‚  Stored inline
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
      Total: 64 bytes + padding

  Plus 3 separate heap objects for strings
  Plus 1 separate Department object
  = 5 total heap objects for one Employee!

Key insight: Reference types create a graph of objects, not a single contiguous block. This has implications:

  • Cache locality: Related data scattered across heap = more cache misses
  • Garbage collection: GC must traverse all references
  • Memory fragmentation: Objects allocated at different times may be far apart

πŸ”§ Try this: Use a memory profiler (Visual Studio Diagnostic Tools, dotMemory, or PerfView) to visualize your application's object graph and identify memory hotspots.

Common Mistakes ⚠️

1. Confusing Stack Overflow with Out of Memory

Stack overflow happens when you exceed stack space (~1MB):

// Infinite recursion β†’ stack overflow
public void RecursiveMethod()
{
    RecursiveMethod();  // Each call adds stack frame
}

Out of memory happens when heap is exhausted (GB of memory):

// Creates massive heap allocations
List<byte[]> memory = new List<byte[]>();
while (true)
{
    memory.Add(new byte[1024 * 1024]);  // 1MB per iteration
}

2. Assuming Structs Are Always Faster

Structs avoid heap allocation, but they're copied on every assignment:

public struct LargeStruct
{
    public long Field1, Field2, Field3, Field4;  // 32 bytes
    // ... 20 more fields β†’ 200 bytes total
}

// ⚠️ This copies 200 bytes!
public void ProcessData(LargeStruct data)  
{
    // Working with a COPY, not original
}

// βœ… Better: pass by reference (no copy)
public void ProcessData(ref LargeStruct data)
{
    // Working with original via pointer
}

Rule of thumb: Keep structs small (<= 16 bytes). For larger data, use classes.

3. Forgetting String Immutability

// ⚠️ Creates 10,000 string objects in heap!
string result = "";
for (int i = 0; i < 10000; i++)
{
    result += i.ToString();  // New string each time
}

// βœ… StringBuilder reuses buffer
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++)
{
    sb.Append(i);  // Modifies existing buffer
}
string result = sb.ToString();

Each string concatenation creates a new object because strings are immutable. Use StringBuilder for repeated modifications.

4. Creating Unnecessary Object References

// ⚠️ Keeps objects alive longer than needed
public class DataProcessor
{
    private List<byte[]> processedData = new List<byte[]>();
    
    public void ProcessLargeFile()
    {
        for (int i = 0; i < 1000; i++)
        {
            byte[] chunk = ReadChunk();
            Process(chunk);
            processedData.Add(chunk);  // Prevents GC!
        }
        // Chunks can't be collected until list is cleared
    }
}

// βœ… Let chunks be collected immediately
public void ProcessLargeFile()
{
    for (int i = 0; i < 1000; i++)
    {
        byte[] chunk = ReadChunk();
        Process(chunk);
        // chunk becomes eligible for GC after this iteration
    }
}

5. Misunderstanding Nullable Value Types

int? nullableInt = null;  // What's in memory?

int? is actually Nullable<int>, a struct:

public struct Nullable<T> where T : struct
{
    private bool hasValue;  // 1 byte (+ 3 padding)
    private T value;        // 4 bytes for int
}
// Total: 8 bytes (vs 4 for plain int)

Nullable types double memory usage for small types and still live on the stack (unless boxed).

Key Takeaways 🎯

πŸ“‹ Memory Fundamentals Quick Reference

Stack Fast, automatic, LIFO, ~1MB, method locals
Heap Flexible, GC-managed, GB+, objects
Value Types Data stored directly, copied by value, stack or inline
Reference Types Store pointer, copied by reference, always heap
Boxing Value β†’ Object wrapping, expensive, avoid in loops
Object Overhead 16 bytes minimum (header + method table ptr)
Struct Best Practice Keep small (≀16 bytes), immutable, logically atomic
String Concatenation Use StringBuilder for repeated modifications

Mental Model Summary:

  • Stack = Method execution workspace (automatic cleanup)
  • Heap = Long-lived object storage (garbage collected)
  • Value types = Lightweight data (direct storage)
  • Reference types = Complex objects (indirect via pointer)

Performance Principles:

  1. Stack allocation is ~100x faster than heap allocation
  2. Small, short-lived data β†’ value types
  3. Large, long-lived, or shared data β†’ reference types
  4. Avoid boxing in performance-critical code
  5. Minimize object allocations in hot paths

Design Guidelines:

  • Use structs for small, immutable data that behaves like a value
  • Use classes for complex entities with identity and mutable state
  • Be mindful of reference aliasing when passing objects
  • Profile memory usageβ€”intuition can be wrong!

πŸ“š Further Study

  1. Microsoft Documentation - Memory Management: https://learn.microsoft.com/en-us/dotnet/standard/automatic-memory-management
  2. Pro .NET Memory Management (Book): https://prodotnetmemory.com/
  3. Performance Profiling with PerfView: https://github.com/microsoft/perfview

Understanding these memory fundamentals prepares you for advanced topics like garbage collection generations, large object heap behavior, memory pressure, and performance optimization techniques. The next lesson will explore how the .NET Garbage Collector manages heap memory and the strategies you can employ to work with it efficiently. πŸš€