Module 1 · How Boxing and Unboxing Work · Lesson 2 of 21
Boxing step by step
2.1 The smallest example
int number = 42; object boxed = number; // Implicit boxing conversion. number = 100; Console.WriteLine(number); // 100 Console.WriteLine(boxed); // 42
The assignment to boxed does not make an alias to the local variable number. It snapshots the current four payload bytes into a new object. Changing the local later changes only the local.
STEP 1 - before boxing
Thread stack / JIT local state
+----------------------------------+ | number | | | | int32 bytes = 2A 00 00 00 | | | +----------------------------------+
Managed heap
Gen 0 allocation area: [ free ... ]
STEP 2 - execute: object boxed = number;
Thread stack / roots
Managed heap - Gen 0
+----------------------------------+ allocation start | number = 42 | | +---------------------------+ -8 | boxed -------------------------------| object header | +----------------------------------+ +---------------------------+ 0 <- boxed | MethodTable* : Int32 box | +---------------------------+ +8 | int32 payload = 42 | +---------------------------+ +12 | padding | +---------------------------+ total commonly 24 B
STEP 3 - number = 100;
The local changes. The independent boxed payload remains 42.
2.2 Representative allocation arithmetic
On a typical 64-bit CoreCLR process, a boxed int commonly consumes the minimum object size: approximately 24 bytes.
| Component | Logical bytes | Representative aligned bytes |
|---|---|---|
| Object header before managed reference | 8 | 8 |
| MethodTable pointer | 8 | 8 |
| int payload | 4 | 4 |
| Alignment / minimum-object padding | 0 | 4 |
| Total allocation | 20 | 24 |
That is roughly six times the four payload bytes, before counting the reference slot that stores the box elsewhere. In an object[] on x64, the element slot adds another 8 bytes.
Cost model for one box
- Time: O(1). Allocate, initialize metadata, copy the value payload, return a reference.
- Allocation: Usually one managed object. Payload size is rounded for object alignment and minimum size.
- GC impact: One more object to discover, age, possibly promote, and eventually reclaim. A box with no reference fields is cheap to scan, but it still occupies heap space and contributes to collection frequency.
- Locality: The value is no longer inline with neighboring values. Consumers follow a reference to a separate object.
2.3 Boxes preserve runtime type, not source spelling
byte small = 7; object a = small; // Boxed System.Byte. object b = (int)small; // Numeric conversion first, then boxed System.Int32. Console.WriteLine(a.GetType()); // System.Byte Console.WriteLine(b.GetType()); // System.Int32
The object records the exact boxed value type through its MethodTable. This exactness becomes crucial during unboxing.
Under The Hood
- IL stack transition: Conceptually,
box Tconsumes a value of typeTfrom the evaluation stack and pushes an object reference. - MethodTable: The boxed object points to runtime metadata for the boxed value type. The runtime can therefore answer
GetType(), dispatch overridden methods, and obtain the GC layout for a boxed struct containing references. - Copy size: For a struct, all instance fields are copied into the payload. A large struct makes boxing more expensive than boxing a four-byte integer.
- Write barriers: If the boxed struct contains managed-reference fields, initializing those references must obey GC write-barrier rules. The MethodTable’s GC descriptor tells the collector which payload words are references.