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:
- Calculate size: The CLR determines how much memory is needed for the object, including its fields and overhead
- Find space: The allocator searches for a contiguous block of free memory on the heap
- Allocate block: Memory is reserved at a specific address
- Initialize header: Every object has a hidden header containing type information and GC metadata
- Call constructor: Your constructor code runs to initialize fields
- 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:
- Check if enough space remains
- Increment the pointer
- 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 Personallocates memory on the heapp1stores a reference (address) to that memoryp2 = p1copies the reference, NOT the object- Both variables point to the same object
- Changing
p2.Ageaffects the same object thatp1references
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:
- Reference types live on the heap - Classes, arrays, delegates, and interfaces are heap-allocated
- Heap allocation is managed - The garbage collector automatically reclaims memory
- References are copied, not objects - Assignment copies the pointer, not the data
- Generations optimize collection - Gen 0 β Gen 1 β Gen 2 based on survival
- Large objects are special - Objects β₯85KB go to the LOH with different rules
- Allocation is fast - Bump pointer allocation makes heap allocation efficient in Gen 0
- Immutability matters - Strings create new objects on modification
- Boxing allocates - Converting value types to
objectcauses heap allocation
π‘ Performance Guidelines:
- Reuse objects when possible to reduce allocation pressure
- Use
StringBuilderfor string manipulation - Prefer
Span<T>andMemory<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
| Concept | Key Point |
|---|---|
| Heap | Dynamic memory for reference types |
| Reference Types | class, interface, delegate, array, string |
| Allocation Cost | Fast in Gen 0 (bump pointer), expensive if GC needed |
| Object Header | 8-16 bytes (sync block + method table pointer) |
| Generations | Gen 0 (young) β Gen 1 (buffer) β Gen 2 (old) |
| LOH Threshold | β₯85,000 bytes |
| String Behavior | Reference type but immutable (creates new on modify) |
| Boxing | Value type β object causes heap allocation |
| GC Frequency | Gen 0 (frequent) > Gen 1 (occasional) > Gen 2 (rare) |
| Best Practice | Minimize allocations, reuse objects, pool large buffers |
π Further Study
- Microsoft Docs - Memory Management: https://learn.microsoft.com/en-us/dotnet/standard/garbage-collection/memory-management-and-gc
- CLR via C# by Jeffrey Richter: https://www.microsoftpressstore.com/store/clr-via-c-sharp-9780735667457 (Chapter 21: The Managed Heap)
- 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! π