The Runtime Theory
mediumApplicationDSA#boxing#value-types#memory#generics

Why is boxing slow and when does it happen without you noticing?

Probes the real cost model of boxing: heap allocation, copy of value semantics, and GC pressure — plus the silent boxing sites in collections, generics, and closures.

The Runtime Theory Team2 min readasked at microsoft · google · netflix · datadog

This question tests whether you understand value types vs reference types at the machine level, not just "boxing is slow." The interviewer wants the mechanism: what a box is, what allocation costs, and where the compiler inserts it behind your back.

The mechanism. A value type (an int, a struct) lives inline — in a register, on the stack, or inside the containing object. A reference type is a pointer to heap memory. Boxing converts a value into a reference: the runtime allocates a heap object, copies the value into it, and gives you a pointer. Unboxing is the reverse: check the object is really that type, copy the value back out. The object you get points at the heap.

Why it's slow is three distinct costs, and naming all three is the answer:

  1. Allocation. A heap object requires allocation — even bump allocation costs instructions, and in a GC runtime the box participates in the generational heap and eventually gets collected. In a loop, that's GC pressure per iteration.
  2. The copy. The value is copied into the box and copied out on unboxing. Value semantics mean no aliasing benefit: you boxed a copy, not the original.
  3. The indirection. Every access to the value goes through the pointer, which may be a cache miss — and the box is a separate allocation, so it breaks locality of the containing data structure.

Silent boxing sites — this is the part people miss. In Java (pre-generics especially), ArrayList<Integer> boxes every add/get:

java
List<Integer> nums = new ArrayList<>();
nums.add(1);                 // box: Integer.valueOf(1) on the heap
int x = nums.get(0);         // unbox: (Integer) obj → intValue()

But even with generics: string concatenation boxes primitives; varargs pack arguments into Object[]; comparing a nullable value; storing an int in a Map<String, Object>; and equality checks like Integer with == compare references, not values — the classic IntegerCache interview trap. In C#, object o = 42 boxes, and pre-generics collections boxed everything; C# generics generate specialized code per value type (no boxing), while Java erases generics to Object, so boxing survives. Lambdas add another silent site: a captured int may be boxed into a heap closure object just to outlive the frame.

Edge cases worth naming: the JIT can elide boxing when it can prove the box never escapes (escape analysis, scalar replacement); small cached values (Integer -128..127) make some "boxing" free; and unboxing failure throws a ClassCastException/InvalidCastException — a type check per unbox.

A strong closing: "Boxing is slow because it turns a register-sized value into a heap allocation with a copy and a pointer chase — and the compiler inserts it anywhere a value type meets a reference-typed slot."

This answer walks

Follow-ups they'll push on

  1. 01How do generics avoid boxing in Java, and why does C# differ?
  2. 02What is the cost of unboxing besides the cast?
  3. 03When is boxing actually free or elided by the JIT?
  4. 04How would you find boxing in a hot loop in a profiler?

More interviews in this topic

One dispatch a week

The trace behind each question, the tradeoff that explains it, and one technical dispatch per week — no noise.

One technical dispatch per week. No noise.