C# Memory Layout: Value Types vs Reference Types

In C#, understanding how value types and reference types utilize memory is essential for writing high-performance, efficient code. The core distinction lies in how and where data is physically stored in memory: value types hold their data directly inline wherever they are declared, while reference types store their actual data on the managed heap, accessed via an address pointer. This architectural difference fundamentally shapes memory consumption, object overhead, cache locality, and the operational workload placed on the .NET Garbage Collector (GC).

Value Types: Inline Data Storage

Value types—which include primitives like int, float, and bool, as well as custom struct types and enum types—store their values directly within the memory allocated for the variable.

  • Allocation Location: Value types do not automatically live on the stack. Instead, their allocation is contextual. If declared as a local variable within a method, the data resides directly on the execution stack. If declared as a field inside a class, the value type's data is embedded directly inline within that class instance on the heap.
  • Lack of Metadata Overhead: Value types carry zero inherent memory overhead. A 32-bit integer (int) consumes exactly 4 bytes of memory, and a custom struct containing two int fields consumes 8 bytes (subject to byte alignment and padding). They do not contain any object headers or type pointers.
  • Copy Semantics: When a value type is assigned to another variable or passed by value to a method, a bitwise copy of the actual data is created in a new memory location. Modifying the copy does not alter the original value.

Reference Types: Pointers and the Managed Heap

Reference types—including classes, interfaces, delegates, string, and record class types—are always stored on the managed heap, while the variable itself holds only a memory address (a reference) pointing to that heap location.

  • Split Layout: A reference type involves two distinct memory locations:
    1. The Reference (Pointer): Located on the stack (if local) or inside another object. It consumes 4 bytes on a 32-bit architecture or 8 bytes on a 64-bit architecture.
    2. The Object Instance: Allocated on the managed heap, containing the actual object data.
  • Object Header Overhead: Every reference type instance on the managed heap includes hidden overhead that precedes its actual data fields:
    • SyncBlock Index: Used for thread synchronization (locking), hash codes, and garbage collection flags (4 bytes on 32-bit, 8 bytes on 64-bit).
    • MethodTable Pointer (Type Handle): Points to the type's runtime metadata and virtual method table (4 bytes on 32-bit, 8 bytes on 64-bit).
    Because of this, an empty class instance on a 64-bit runtime immediately consumes at least 24 bytes (16 bytes of header overhead plus the minimum object size alignment), even before any field data is added.
  • Copy Semantics: When assigning a reference type to a new variable, only the memory address (the pointer) is copied. Both variables then point to the exact same memory block on the heap, meaning changes made through one variable affect the other.

Memory Overhead and Cache Locality

The difference in physical layout significantly impacts hardware performance and cache efficiency:

Characteristic Value Type Reference Type
Variable Contents The actual data value A memory address pointing to the heap
Object Overhead 0 bytes 8–16 bytes (SyncBlock + Type Handle)
Default Pointer Size None (unless boxed or passed with ref) 4 bytes (32-bit) / 8 bytes (64-bit)
GC Pressure Minimal to none (unless boxed) High (requires GC tracking and compaction)
Memory Access Direct access Indirect access (pointer dereferencing)

Because arrays of value types store their elements contiguously in a single contiguous block of memory, the CPU can read them sequentially into the L1/L2 hardware caches with minimal cache misses. In contrast, an array of reference types stores an array of pointers; accessing the actual elements requires dereferencing each pointer to scattered locations on the heap, which increases cache misses and latency.