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

Object header and layout

Method table pointer, sync block index, and field alignment

Object Header and Layout in .NET Memory

Understanding how objects are structured in .NET memory is crucial for optimizing performance and memory usage. Master object headers, type information, and memory alignment with free flashcards and spaced repetition practice. This lesson covers the internal structure of .NET objects, memory overhead calculations, and object layout optimizationโ€”essential concepts for building high-performance applications and acing technical interviews.

Welcome ๐Ÿ’ป

Every object you create in .NET carries more than just your data. Behind the scenes, the CLR (Common Language Runtime) adds metadata that enables garbage collection, type safety, synchronization, and polymorphism. This "hidden" overhead can significantly impact memory consumption in data-intensive applications.

In this lesson, we'll dissect a .NET object down to the byte level. You'll learn exactly what gets stored in memory, why objects take more space than you might expect, and how to calculate the true memory footprint of your data structures. Whether you're building high-performance systems, diagnosing memory leaks, or preparing for advanced .NET interviews, understanding object layout is foundational.

Core Concepts: Anatomy of a .NET Object ๐Ÿ”

The Complete Object Structure

When you create an object in .NET, the runtime allocates memory with three distinct sections:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚           .NET OBJECT IN MEMORY            โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚                                            โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”‚
โ”‚  โ”‚  OBJECT HEADER (8-16 bytes)      โ”‚     โ”‚
โ”‚  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค     โ”‚
โ”‚  โ”‚  - Sync Block Index (4 bytes)    โ”‚     โ”‚
โ”‚  โ”‚  - Type Handle (4-8 bytes)       โ”‚     โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ”‚
โ”‚                 โ†“                          โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”‚
โ”‚  โ”‚  INSTANCE FIELDS                 โ”‚     โ”‚
โ”‚  โ”‚  (your actual data)              โ”‚     โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ”‚
โ”‚                 โ†“                          โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”     โ”‚
โ”‚  โ”‚  PADDING (for alignment)         โ”‚     โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜     โ”‚
โ”‚                                            โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Let's examine each component in detail.

1. The Object Header: Control Center ๐ŸŽฏ

Every managed object begins with an object header that contains two critical pieces of information:

Sync Block Index (4 bytes)

This 4-byte field serves multiple purposes:

  • Thread synchronization: When you use lock(object), the CLR needs a place to store synchronization information. The sync block index points to an entry in the sync block table.
  • Hash code caching: When you call GetHashCode(), the result is cached here after the first call.
  • Default value: Initially zero, indicating no sync block is allocated yet (lazy allocation saves memory!).

Type Handle / Method Table Pointer (4-8 bytes)

This field is a pointer to the object's Method Table (also called Type Handle):

  • Enables polymorphism: When you call a virtual method, the CLR uses this pointer to find the correct implementation.
  • Type identification: Operations like is, as, and GetType() use this pointer.
  • Contains static fields: The method table holds references to static data for the type.
  • Platform-dependent size: 4 bytes on 32-bit systems, 8 bytes on 64-bit systems.

๐Ÿ’ก Memory Tip: On 64-bit systems, the object header consumes 12 bytes (4 + 8), but padding often brings it to 16 bytes for alignment.

2. Instance Fields: Your Actual Data ๐Ÿ“Š

After the header comes the memory for your fields, laid out according to these rules:

Field Ordering Rules

  1. Reference types first: Object references (8 bytes on 64-bit, 4 bytes on 32-bit)
  2. Larger value types next: Sorted by descending size (8-byte types, then 4-byte, etc.)
  3. Smaller value types last: Bytes and booleans at the end

Why this order? The CLR optimizes for alignment requirements. Modern CPUs read memory most efficiently when data aligns to natural boundaries (e.g., 8-byte values at addresses divisible by 8).

Field TypeSize (bytes)Typical Placement
Reference (object)4 (32-bit) / 8 (64-bit)First
long, double, decimal8 / 8 / 16Early
int, float, uint4Middle
short, ushort, char2Late
byte, bool, sbyte1Last

Example Object Layout:

public class Person
{
    public string Name;      // Reference: 8 bytes (64-bit)
    public int Age;          // Value: 4 bytes
    public byte Level;       // Value: 1 byte
}
Memory Layout (64-bit):

Offset  Content                    Bytes
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
0       Sync Block Index           4
4       (padding)                  4
8       Type Handle                8
16      Name (reference)           8
24      Age                        4
28      Level                      1
29      (padding to 8-byte align)  3
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
Total: 32 bytes

3. Padding and Alignment: The Hidden Space ๐Ÿ“

The CLR ensures objects align to pointer-sized boundaries (4 bytes on 32-bit, 8 bytes on 64-bit). This means:

  • 64-bit systems: Total object size must be a multiple of 8 bytes
  • 32-bit systems: Total object size must be a multiple of 4 bytes

Padding bytes are added at the end if needed. This "wasted" space ensures the next object in memory starts at a properly aligned address.

๐Ÿง  Memory Device: Think "STOP" = Sync, Type, Object data, Padding

Deep Dive: Calculating Object Size ๐Ÿงฎ

Formula for Object Size

To calculate the exact heap allocation for an object:

๐Ÿ“‹ Object Size Formula

Total Size = Header + Fields + Padding

  • Header = 8 bytes (32-bit) or 12-16 bytes (64-bit, with alignment)
  • Fields = Sum of all instance field sizes
  • Padding = Bytes needed to reach next multiple of pointer size

64-bit Quick Formula:

Size = RoundUpTo8(16 + FieldsSize)

Example 1: Simple Class

public class Point
{
    public int X;  // 4 bytes
    public int Y;  // 4 bytes
}
StepComponentBytes
1Object Header (64-bit)16
2Field X (int)4
3Field Y (int)4
4Subtotal24
5Padding needed0 (already multiple of 8)
Total24 bytes

Example 2: Mixed Field Types

public class Employee
{
    public string Name;     // 8 bytes (reference)
    public int Id;          // 4 bytes
    public bool IsActive;   // 1 byte
}
StepComponentBytesRunning Total
1Object Header1616
2Name (reference)824
3Id (int)428
4IsActive (bool)129
5Padding (to reach 32)332
Total32 bytes

Why 32 and not 29? The next multiple of 8 after 29 is 32, so 3 bytes of padding are added.

Example 3: Empty Class Surprise

public class EmptyClass { }

Even an empty class consumes memory:

  • Header: 16 bytes (64-bit with alignment)
  • Fields: 0 bytes
  • Padding: 0 bytes (16 is already aligned)
  • Total: 16 bytes

๐Ÿ’ก Pro Tip: Every object instance costs at least 16 bytes on 64-bit systems, regardless of content. Creating millions of tiny objects wastes significant memory on overhead.

Special Cases and Advanced Scenarios ๐ŸŽ“

Arrays: Additional Overhead

Arrays are objects too, but with extra metadata:

Array Memory Layout:
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Object Header        (16 bytes)โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  Type Handle         (8 bytes) โ”‚ โ† points to array type
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  Length              (4 bytes) โ”‚ โ† array.Length
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  (padding)           (4 bytes) โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  Element[0]                    โ”‚
โ”‚  Element[1]                    โ”‚
โ”‚  ...                           โ”‚
โ”‚  Element[n-1]                  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Array size formula (64-bit):

Size = RoundUpTo8(24 + (ElementSize ร— Length))

Example: int[] numbers = new int[10];

  • Header: 16 bytes
  • Length field: 8 bytes (including padding)
  • Elements: 4 ร— 10 = 40 bytes
  • Total: 64 bytes

Strings: Special Reference Type

Strings have a unique layout:

string text = "Hello";
String Memory Layout:
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Object Header        16 bytes โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  Length (int)         4 bytes  โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  First char           2 bytes  โ”‚ โ† 'H'
โ”‚  Second char          2 bytes  โ”‚ โ† 'e'
โ”‚  ...                           โ”‚
โ”‚  Null terminator      2 bytes  โ”‚ โ† '\0'
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

String size formula:

Size = RoundUpTo8(20 + (Length ร— 2) + 2)

For "Hello" (5 characters):

  • Header: 16 bytes
  • Length field: 4 bytes
  • Characters: 5 ร— 2 = 10 bytes
  • Null terminator: 2 bytes
  • Subtotal: 32 bytes (already aligned)

Value Types on the Heap

Value types (structs) normally live on the stack, but they end up on the heap when:

  1. Boxed: object obj = 42; (int boxed into object)
  2. Field of a class: The struct data becomes part of the class's memory
  3. Array element: Point[] points = new Point[5];

Boxing overhead example:

int value = 42;          // Stack: 4 bytes
object boxed = value;    // Heap: 16 (header) + 4 (int) + 4 (padding) = 24 bytes

Boxing adds 20 bytes of overhead! This is why frequent boxing/unboxing hurts performance.

Structure Packing and LayoutKind

You can control field layout with attributes:

[StructLayout(LayoutKind.Sequential)]
public struct Sequential
{
    public byte A;   // Offset 0
    public int B;    // Offset 4 (not 1, due to alignment)
    public byte C;   // Offset 8
}

[StructLayout(LayoutKind.Explicit)]
public struct Explicit
{
    [FieldOffset(0)] public byte A;
    [FieldOffset(1)] public int B;   // You control exact position
    [FieldOffset(5)] public byte C;
}

โš ๏ธ Warning: Explicit layout can save memory but requires careful management to avoid data corruption and alignment issues.

Real-World Examples ๐ŸŒ

Example 1: Cache Line Optimization

Problem: You're building a high-frequency trading system processing millions of ticks per second.

// Inefficient: Fields split across cache lines
public class TradeTick
{
    public DateTime Timestamp;     // 8 bytes
    public decimal Price;          // 16 bytes
    public int Volume;             // 4 bytes
    public string Symbol;          // 8 bytes (reference)
    public bool IsBuy;             // 1 byte
}

Memory layout:

  • Header: 16 bytes (offsets 0-15)
  • Timestamp: 8 bytes (offsets 16-23)
  • Price: 16 bytes (offsets 24-39)
  • Volume: 4 bytes (offsets 40-43)
  • Symbol: 8 bytes (offsets 44-51)
  • IsBuy: 1 byte (offset 52)
  • Padding: 3 bytes (offsets 53-55)
  • Total: 56 bytes

On modern CPUs, cache lines are typically 64 bytes. This object almost fits in one cache line but not quite. Frequently accessed fields might span cache lines, causing performance degradation.

Solution: Group frequently accessed fields together:

public class OptimizedTradeTick
{
    // Hot path fields (accessed every tick)
    public decimal Price;          // 16 bytes
    public int Volume;             // 4 bytes
    public bool IsBuy;             // 1 byte
    // Padding here
    
    // Cold path fields (accessed less frequently)
    public DateTime Timestamp;     // 8 bytes
    public string Symbol;          // 8 bytes
}

Example 2: Memory-Efficient Collections

Scenario: Storing 1 million user sessions.

// Wasteful: 48 bytes ร— 1M = 48 MB just for objects
public class UserSession
{
    public string UserId;          // 8 bytes reference
    public DateTime LastActive;    // 8 bytes
    public int RequestCount;       // 4 bytes
    // Header: 16 bytes, Total: 36 โ†’ 40 with padding
}

var sessions = new UserSession[1_000_000];

Total memory:

  • Array overhead: 24 bytes
  • Array elements (references): 8 ร— 1M = 8 MB
  • Objects: 40 ร— 1M = 40 MB
  • Total: ~48 MB (not counting string data)

Optimized with structs:

public struct UserSessionData  // No heap object header!
{
    public int UserIdHash;         // 4 bytes (hash instead of reference)
    public long LastActiveTicks;   // 8 bytes
    public int RequestCount;       // 4 bytes
    // Total: 16 bytes, no padding needed
}

var sessions = new UserSessionData[1_000_000];

New total memory:

  • Array overhead: 24 bytes
  • Array elements (inline): 16 ร— 1M = 16 MB
  • Total: ~16 MB (67% reduction!)

๐Ÿ’ก When to use this pattern: Large collections of small, short-lived data where identity isn't important.

Example 3: Diagnostic Tool

Here's a practical utility to measure object size:

using System;
using System.Runtime.InteropServices;

public static class ObjectSizeCalculator
{
    public static long GetSize<T>(T obj) where T : class
    {
        if (obj == null) return 0;
        
        long size = IntPtr.Size == 8 ? 16 : 8; // Header
        
        var type = typeof(T);
        foreach (var field in type.GetFields(
            System.Reflection.BindingFlags.Instance | 
            System.Reflection.BindingFlags.Public | 
            System.Reflection.BindingFlags.NonPublic))
        {
            size += GetFieldSize(field.FieldType);
        }
        
        // Round up to pointer size
        long alignment = IntPtr.Size;
        return (size + alignment - 1) / alignment * alignment;
    }
    
    private static int GetFieldSize(Type type)
    {
        if (!type.IsValueType) return IntPtr.Size; // Reference
        return Marshal.SizeOf(type);
    }
}

// Usage:
var employee = new Employee();
Console.WriteLine($"Employee size: {ObjectSizeCalculator.GetSize(employee)} bytes");

๐Ÿ”ง Try this: Run this on your own classes to understand their memory footprint!

Example 4: Struct vs Class Decision

Guidelines for choosing:

Use Struct When...Use Class When...
โœ… Size โ‰ค 16 bytesโœ… Size > 16 bytes
โœ… Immutable dataโœ… Mutable state
โœ… Short-livedโœ… Long-lived
โœ… No identity neededโœ… Identity important
โœ… Value semanticsโœ… Reference semantics
Example: Point, ColorExample: Customer, Order

Bad struct example:

// DON'T: Large mutable struct
public struct HugeData  // 80 bytes!
{
    public decimal Field1, Field2, Field3, Field4, Field5;
    // Copying this is expensive!
}

void Process(HugeData data)  // Copies 80 bytes to stack!
{
    // ...
}

Fixed:

// DO: Use class for large data
public class HugeData  // Pass by reference (8 bytes)
{
    public decimal Field1, Field2, Field3, Field4, Field5;
}

void Process(HugeData data)  // Copies 8-byte reference
{
    // ...
}

Common Mistakes โš ๏ธ

Mistake 1: Ignoring Memory Overhead

โŒ Wrong Assumption:

// "This only uses 4 bytes per object"
public class Counter
{
    public int Value;  // 4 bytes
}

var counters = new Counter[1_000_000];
// Actual: (16 header + 4 field + 4 padding) ร— 1M = 24 MB
// Expected: 4 MB

โœ… Correct Approach:

// Use array of structs or value type collection
var counters = new int[1_000_000];
// Actual: 24 header + (4 ร— 1M) = 4 MB + tiny overhead

Mistake 2: Field Ordering Doesn't Matter?

โŒ Inefficient:

public class DataPoint
{
    public byte Flag1;      // 1 byte
    public long Value1;     // Padding added before this!
    public byte Flag2;      // 1 byte
    public long Value2;     // Padding added before this!
}
// Total: 16 (header) + 1 + 7 (pad) + 8 + 1 + 7 (pad) + 8 = 48 bytes!

โœ… Optimized:

public class DataPoint
{
    public long Value1;     // 8 bytes
    public long Value2;     // 8 bytes
    public byte Flag1;      // 1 byte
    public byte Flag2;      // 1 byte
}
// Total: 16 (header) + 8 + 8 + 1 + 1 + 6 (pad) = 40 bytes (17% reduction)

Mistake 3: Boxing in Hot Paths

โŒ Performance Killer:

var list = new ArrayList();  // Stores objects
for (int i = 0; i < 1_000_000; i++)
{
    list.Add(i);  // Boxes each int! 1M allocations ร— 24 bytes
}

โœ… Correct:

var list = new List<int>();  // Type-safe, no boxing
for (int i = 0; i < 1_000_000; i++)
{
    list.Add(i);  // No boxing, values stored inline
}

Mistake 4: Empty Base Classes

โŒ Wasteful Inheritance:

public class Entity { }  // 16 bytes overhead

public class Customer : Entity  // Still 16 bytes header
{
    public int Id;  // Total: 24 bytes
}

// 1M customers = 24 MB instead of potential 8 MB

โœ… Better:

public interface IEntity { }  // No memory cost

public class Customer : IEntity
{
    public int Id;  // Total: 16 (header) + 4 + 4 (pad) = 24 bytes
    // Same size, but more flexible
}

Mistake 5: Not Considering Object Lifetime

โŒ Short-Lived Heap Allocations:

for (int i = 0; i < 1_000_000; i++)
{
    var point = new Point { X = i, Y = i * 2 };  // 1M heap allocations
    ProcessPoint(point);
}

โœ… Stack Allocation with Structs:

for (int i = 0; i < 1_000_000; i++)
{
    var point = new PointStruct { X = i, Y = i * 2 };  // Stack allocated
    ProcessPoint(point);
}  // No GC pressure!

Key Takeaways ๐ŸŽฏ

๐Ÿ“‹ Quick Reference Card: Object Memory Layout

Component32-bit64-bitPurpose
Sync Block Index4 bytes4 bytesLocking, hash codes
Type Handle4 bytes8 bytesType info, polymorphism
Instance FieldsVariesVariesYour data
Padding0-3 bytes0-7 bytesAlignment
Min Object Size12 bytes24 bytesEmpty class

Key Formulas:

  • Object Size (64-bit): RoundUpTo8(16 + FieldsSize)
  • Array Size (64-bit): RoundUpTo8(24 + ElementSize ร— Length)
  • String Size: RoundUpTo8(20 + Length ร— 2 + 2)

Memory Rules:

  1. Every object has โ‰ฅ16-byte overhead (64-bit)
  2. References are always pointer-sized (8 bytes on 64-bit)
  3. Fields are ordered: references โ†’ large values โ†’ small values
  4. Alignment requires padding to pointer-size multiples
  5. Boxing adds full object header to value types

Optimization Checklist:

โœ… Group related fields by size (largest first)
โœ… Use structs for small (<16 bytes), immutable data
โœ… Avoid boxing in loops and hot paths
โœ… Consider arrays of structs vs. arrays of classes
โœ… Measure actual memory usage with profilers
โœ… Be aware of cache line boundaries (64 bytes)

๐Ÿค” Did You Know?

The .NET runtime uses object header compression in some scenarios. When an object is in Gen0 (newly created), the GC might use special encoding tricks to reduce header size temporarily. However, once objects survive to Gen1/Gen2, they get full headers. This is why Gen0 collections are so fastโ€”smaller objects mean less memory to scan!

๐Ÿ’ก Pro Tips

  1. Use Memory Profilers: Tools like dotMemory, ANTS Memory Profiler, or Visual Studio's memory tools show actual object sizes and counts.

  2. Benchmark Before Optimizing: Use BenchmarkDotNet to measure if your "optimization" actually helps.

  3. Watch for Collection Overhead: A List<T> has its own object header plus internal array. The array also has overhead. Nested collections multiply memory costs.

  4. Consider Span and Memory: These modern types provide views over memory without allocations, perfect for performance-critical code.

  5. Object Pools: For frequently allocated short-lived objects, pooling can eliminate allocation overhead entirely.

๐Ÿ“š Further Study

Deepen your understanding with these resources:

  1. Microsoft Docs - Memory Management in .NET: https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/memory-management-and-gc
  2. CLR via C# by Jeffrey Richter: The definitive guide to CLR internals, with extensive coverage of object layout (https://www.amazon.com/CLR-via-C-Jeffrey-Richter/dp/0735667454)
  3. Pro .NET Memory Management by Konrad Kokosa: Comprehensive deep-dive into .NET memory mechanics (https://prodotnetmemory.com)

Master these concepts, and you'll write more efficient .NET code while confidently tackling memory-related performance challenges! ๐Ÿš€